release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
namespace StellaOps.AdvisoryAI.Plugin.Unified;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using StellaOps.AdvisoryAI.Inference.LlmProviders;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Capabilities;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
using StellaOps.Plugin.Abstractions.Health;
|
||||
using StellaOps.Plugin.Abstractions.Lifecycle;
|
||||
|
||||
// Type aliases to disambiguate between AdvisoryAI and Plugin.Abstractions types
|
||||
using AdvisoryLlmRequest = StellaOps.AdvisoryAI.Inference.LlmProviders.LlmCompletionRequest;
|
||||
using AdvisoryLlmResult = StellaOps.AdvisoryAI.Inference.LlmProviders.LlmCompletionResult;
|
||||
using AdvisoryStreamChunk = StellaOps.AdvisoryAI.Inference.LlmProviders.LlmStreamChunk;
|
||||
using PluginLlmRequest = StellaOps.Plugin.Abstractions.Capabilities.LlmCompletionRequest;
|
||||
using PluginLlmResult = StellaOps.Plugin.Abstractions.Capabilities.LlmCompletionResult;
|
||||
using PluginStreamChunk = StellaOps.Plugin.Abstractions.Capabilities.LlmStreamChunk;
|
||||
|
||||
/// <summary>
|
||||
/// Adapts an existing ILlmProvider to the unified IPlugin and ILlmCapability interfaces.
|
||||
/// This enables gradual migration of AdvisoryAI LLM providers to the unified plugin architecture.
|
||||
/// </summary>
|
||||
public sealed class LlmPluginAdapter : IPlugin, ILlmCapability
|
||||
{
|
||||
private readonly ILlmProvider _inner;
|
||||
private readonly ILlmProviderPlugin _plugin;
|
||||
private readonly int _priority;
|
||||
private IPluginContext? _context;
|
||||
private PluginLifecycleState _state = PluginLifecycleState.Discovered;
|
||||
private List<LlmModelInfo> _models = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new adapter for an existing LLM provider.
|
||||
/// </summary>
|
||||
/// <param name="inner">The existing LLM provider to wrap.</param>
|
||||
/// <param name="plugin">The plugin metadata for this provider.</param>
|
||||
/// <param name="priority">Provider priority (higher = preferred).</param>
|
||||
public LlmPluginAdapter(ILlmProvider inner, ILlmProviderPlugin plugin, int priority = 10)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
_plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
|
||||
_priority = priority;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginInfo Info => new(
|
||||
Id: $"com.stellaops.llm.{_inner.ProviderId}",
|
||||
Name: _plugin.DisplayName,
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: _plugin.Description);
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginCapabilities Capabilities => PluginCapabilities.Llm | PluginCapabilities.Network;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginLifecycleState State => _state;
|
||||
|
||||
#region ILlmCapability
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ProviderId => _inner.ProviderId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Priority => _priority;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<LlmModelInfo> AvailableModels => _models;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> IsAvailableAsync(CancellationToken ct)
|
||||
{
|
||||
return _inner.IsAvailableAsync(ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<PluginLlmResult> CompleteAsync(PluginLlmRequest request, CancellationToken ct)
|
||||
{
|
||||
var advisoryRequest = ToAdvisoryRequest(request);
|
||||
var result = await _inner.CompleteAsync(advisoryRequest, ct);
|
||||
return ToPluginResult(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAsyncEnumerable<PluginStreamChunk> CompleteStreamAsync(PluginLlmRequest request, CancellationToken ct)
|
||||
{
|
||||
var advisoryRequest = ToAdvisoryRequest(request);
|
||||
return StreamAdapter(_inner.CompleteStreamAsync(advisoryRequest, ct), ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<LlmEmbeddingResult?> EmbedAsync(string text, CancellationToken ct)
|
||||
{
|
||||
// Embedding is not supported by the base ILlmProvider interface
|
||||
// Specific providers that support embedding would need custom adapters
|
||||
return Task.FromResult<LlmEmbeddingResult?>(null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPlugin
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
|
||||
{
|
||||
_context = context;
|
||||
_state = PluginLifecycleState.Initializing;
|
||||
|
||||
// Check if the provider is available
|
||||
var available = await _inner.IsAvailableAsync(ct);
|
||||
if (!available)
|
||||
{
|
||||
_state = PluginLifecycleState.Failed;
|
||||
throw new InvalidOperationException($"LLM provider '{_inner.ProviderId}' is not available");
|
||||
}
|
||||
|
||||
// Initialize with a default model entry (provider-specific models would be discovered at runtime)
|
||||
_models = new List<LlmModelInfo>
|
||||
{
|
||||
new(
|
||||
Id: _inner.ProviderId,
|
||||
Name: _plugin.DisplayName,
|
||||
Description: _plugin.Description,
|
||||
ParameterCount: null,
|
||||
ContextLength: null,
|
||||
Capabilities: new[] { "chat", "completion" })
|
||||
};
|
||||
|
||||
_state = PluginLifecycleState.Active;
|
||||
context.Logger.Info("LLM plugin adapter initialized for {ProviderId}", _inner.ProviderId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var available = await _inner.IsAvailableAsync(ct);
|
||||
|
||||
if (available)
|
||||
{
|
||||
return HealthCheckResult.Healthy()
|
||||
.WithDetails(new Dictionary<string, object>
|
||||
{
|
||||
["providerId"] = _inner.ProviderId,
|
||||
["priority"] = _priority
|
||||
});
|
||||
}
|
||||
|
||||
return HealthCheckResult.Unhealthy($"LLM provider '{_inner.ProviderId}' is not available");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_state = PluginLifecycleState.Stopped;
|
||||
_inner.Dispose();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Type Mapping
|
||||
|
||||
private static AdvisoryLlmRequest ToAdvisoryRequest(PluginLlmRequest request)
|
||||
{
|
||||
return new AdvisoryLlmRequest
|
||||
{
|
||||
UserPrompt = request.UserPrompt,
|
||||
SystemPrompt = request.SystemPrompt,
|
||||
Model = request.Model,
|
||||
Temperature = request.Temperature,
|
||||
MaxTokens = request.MaxTokens,
|
||||
Seed = request.Seed,
|
||||
StopSequences = request.StopSequences,
|
||||
RequestId = request.RequestId
|
||||
};
|
||||
}
|
||||
|
||||
private static PluginLlmResult ToPluginResult(AdvisoryLlmResult result)
|
||||
{
|
||||
return new PluginLlmResult(
|
||||
Content: result.Content,
|
||||
ModelId: result.ModelId,
|
||||
ProviderId: result.ProviderId,
|
||||
InputTokens: result.InputTokens,
|
||||
OutputTokens: result.OutputTokens,
|
||||
TimeToFirstTokenMs: result.TimeToFirstTokenMs,
|
||||
TotalTimeMs: result.TotalTimeMs,
|
||||
FinishReason: result.FinishReason,
|
||||
Deterministic: result.Deterministic,
|
||||
RequestId: result.RequestId);
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<PluginStreamChunk> StreamAdapter(
|
||||
IAsyncEnumerable<AdvisoryStreamChunk> source,
|
||||
[EnumeratorCancellation] CancellationToken ct)
|
||||
{
|
||||
await foreach (var chunk in source.WithCancellation(ct))
|
||||
{
|
||||
yield return new PluginStreamChunk(
|
||||
Content: chunk.Content,
|
||||
IsFinal: chunk.IsFinal,
|
||||
FinishReason: chunk.FinishReason);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
namespace StellaOps.AdvisoryAI.Plugin.Unified;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.AdvisoryAI.Inference.LlmProviders;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Capabilities;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating unified LLM plugin adapters from existing providers.
|
||||
/// </summary>
|
||||
public sealed class LlmPluginAdapterFactory
|
||||
{
|
||||
private readonly LlmProviderCatalog _catalog;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly Dictionary<string, LlmPluginAdapter> _adapters = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new factory instance.
|
||||
/// </summary>
|
||||
/// <param name="catalog">The LLM provider catalog.</param>
|
||||
/// <param name="serviceProvider">Service provider for creating providers.</param>
|
||||
public LlmPluginAdapterFactory(LlmProviderCatalog catalog, IServiceProvider serviceProvider)
|
||||
{
|
||||
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available unified LLM plugins.
|
||||
/// </summary>
|
||||
/// <returns>List of unified LLM plugins.</returns>
|
||||
public IReadOnlyList<IPlugin> GetAllPlugins()
|
||||
{
|
||||
var plugins = _catalog.GetAvailablePlugins(_serviceProvider);
|
||||
var result = new List<IPlugin>();
|
||||
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
var adapter = GetOrCreateAdapter(plugin.ProviderId);
|
||||
if (adapter != null)
|
||||
{
|
||||
result.Add(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unified LLM plugin by provider ID.
|
||||
/// </summary>
|
||||
/// <param name="providerId">Provider identifier.</param>
|
||||
/// <returns>Unified LLM plugin, or null if not found.</returns>
|
||||
public IPlugin? GetPlugin(string providerId)
|
||||
{
|
||||
return GetOrCreateAdapter(providerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the LLM capability for a provider.
|
||||
/// </summary>
|
||||
/// <param name="providerId">Provider identifier.</param>
|
||||
/// <returns>LLM capability, or null if not found.</returns>
|
||||
public ILlmCapability? GetCapability(string providerId)
|
||||
{
|
||||
return GetOrCreateAdapter(providerId);
|
||||
}
|
||||
|
||||
private LlmPluginAdapter? GetOrCreateAdapter(string providerId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_adapters.TryGetValue(providerId, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var plugin = _catalog.GetPlugin(providerId);
|
||||
if (plugin == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = _catalog.GetConfiguration(providerId);
|
||||
if (config == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var validation = plugin.ValidateConfiguration(config);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var provider = plugin.Create(_serviceProvider, config);
|
||||
var priority = GetPriority(providerId);
|
||||
|
||||
var adapter = new LlmPluginAdapter(provider, plugin, priority);
|
||||
_adapters[providerId] = adapter;
|
||||
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetPriority(string providerId)
|
||||
{
|
||||
// Default priorities (higher = preferred for local/airgap scenarios)
|
||||
return providerId.ToLowerInvariant() switch
|
||||
{
|
||||
"llama-server" => 100, // Local, highest priority for airgap
|
||||
"ollama" => 90, // Local
|
||||
"claude" => 20, // Remote, lower priority
|
||||
"openai" => 10, // Remote, lowest priority
|
||||
_ => 50
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering unified LLM plugin services.
|
||||
/// </summary>
|
||||
public static class LlmPluginAdapterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds unified LLM plugin adapter services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddUnifiedLlmPlugins(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<LlmPluginAdapterFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Description>Unified plugin adapter for AdvisoryAI LLM providers</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||
<ProjectReference Include="..\..\Plugin\StellaOps.Plugin.Abstractions\StellaOps.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,347 @@
|
||||
namespace StellaOps.AdvisoryAI.Scm.Plugin.Unified;
|
||||
|
||||
using StellaOps.AdvisoryAI.Remediation.ScmConnector;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Capabilities;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
using StellaOps.Plugin.Abstractions.Health;
|
||||
using StellaOps.Plugin.Abstractions.Lifecycle;
|
||||
|
||||
// Type aliases to disambiguate between AdvisoryAI and Plugin.Abstractions types
|
||||
using AdvisoryBranchResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.BranchResult;
|
||||
using AdvisoryFileUpdateResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.FileUpdateResult;
|
||||
using AdvisoryPrCreateResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.PrCreateResult;
|
||||
using AdvisoryPrStatusResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.PrStatusResult;
|
||||
using AdvisoryCiStatusResult = StellaOps.AdvisoryAI.Remediation.ScmConnector.CiStatusResult;
|
||||
using AdvisoryCiCheck = StellaOps.AdvisoryAI.Remediation.ScmConnector.CiCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Adapts an existing IScmConnector to the unified IPlugin and IScmCapability interfaces.
|
||||
/// This enables gradual migration of AdvisoryAI SCM connectors to the unified plugin architecture.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The AdvisoryAI IScmConnector focuses on PR/write operations while IScmCapability focuses on
|
||||
/// read operations. This adapter bridges both, implementing what it can from the underlying connector.
|
||||
/// </remarks>
|
||||
public sealed class ScmPluginAdapter : IPlugin, IScmCapability
|
||||
{
|
||||
private readonly IScmConnector _inner;
|
||||
private readonly IScmConnectorPlugin _plugin;
|
||||
private readonly ScmConnectorOptions _options;
|
||||
private IPluginContext? _context;
|
||||
private PluginLifecycleState _state = PluginLifecycleState.Discovered;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new adapter for an existing SCM connector.
|
||||
/// </summary>
|
||||
/// <param name="inner">The existing SCM connector to wrap.</param>
|
||||
/// <param name="plugin">The plugin metadata for this connector.</param>
|
||||
/// <param name="options">Connector configuration options.</param>
|
||||
public ScmPluginAdapter(IScmConnector inner, IScmConnectorPlugin plugin, ScmConnectorOptions options)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
_plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginInfo Info => new(
|
||||
Id: $"com.stellaops.scm.{_inner.ScmType}",
|
||||
Name: $"{_plugin.DisplayName} SCM Connector",
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: $"{_plugin.DisplayName} source control management connector");
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginTrustLevel TrustLevel => PluginTrustLevel.BuiltIn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginCapabilities Capabilities => PluginCapabilities.Scm | PluginCapabilities.Network;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginLifecycleState State => _state;
|
||||
|
||||
#region IScmCapability - Core properties
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ConnectorType => $"scm.{_inner.ScmType}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => _plugin.DisplayName;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ScmType => _inner.ScmType;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IScmCapability - Connection
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string repositoryUrl)
|
||||
{
|
||||
return _plugin.CanHandle(repositoryUrl);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ConnectionTestResult> TestConnectionAsync(CancellationToken ct)
|
||||
{
|
||||
// Try to perform any operation to test connectivity
|
||||
// Since IScmConnector doesn't have a dedicated test method, we assume success
|
||||
// if the connector was created successfully
|
||||
await Task.Delay(0, ct);
|
||||
return ConnectionTestResult.Succeeded(TimeSpan.FromMilliseconds(1));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ConnectionInfo> GetConnectionInfoAsync(CancellationToken ct)
|
||||
{
|
||||
await Task.Delay(0, ct);
|
||||
return new ConnectionInfo(
|
||||
EndpointUrl: _options.BaseUrl ?? $"https://api.{_inner.ScmType}.com",
|
||||
AuthenticatedAs: "configured-token",
|
||||
Metadata: new Dictionary<string, object>
|
||||
{
|
||||
["scmType"] = _inner.ScmType,
|
||||
["hasToken"] = !string.IsNullOrEmpty(_options.ApiToken)
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IScmCapability - Read operations (limited support via IScmConnector)
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ScmBranch>> ListBranchesAsync(string repositoryUrl, CancellationToken ct)
|
||||
{
|
||||
// IScmConnector doesn't support listing branches
|
||||
// This would require extending the adapter or using direct API calls
|
||||
throw new NotSupportedException(
|
||||
"Branch listing not supported via IScmConnector adapter. " +
|
||||
"Use the IScmCapability-native implementation or extend this adapter.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<ScmCommit>> ListCommitsAsync(
|
||||
string repositoryUrl,
|
||||
string branch,
|
||||
int limit = 50,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Commit listing not supported via IScmConnector adapter.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ScmCommit> GetCommitAsync(string repositoryUrl, string commitSha, CancellationToken ct)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Commit retrieval not supported via IScmConnector adapter.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ScmFileContent> GetFileAsync(
|
||||
string repositoryUrl,
|
||||
string filePath,
|
||||
string? reference = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"File retrieval not supported via IScmConnector adapter.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<Stream> GetArchiveAsync(
|
||||
string repositoryUrl,
|
||||
string reference,
|
||||
ArchiveFormat format = ArchiveFormat.TarGz,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Archive download not supported via IScmConnector adapter.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ScmWebhook> UpsertWebhookAsync(
|
||||
string repositoryUrl,
|
||||
ScmWebhookConfig config,
|
||||
CancellationToken ct)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"Webhook management not supported via IScmConnector adapter.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ScmUser> GetCurrentUserAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
"User info retrieval not supported via IScmConnector adapter.");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Extended SCM operations (from IScmConnector)
|
||||
|
||||
/// <summary>
|
||||
/// Creates a branch from the base branch.
|
||||
/// </summary>
|
||||
public async Task<AdvisoryBranchResult> CreateBranchAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
string branchName,
|
||||
string baseBranch,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.CreateBranchAsync(owner, repo, branchName, baseBranch, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates or creates a file in a branch.
|
||||
/// </summary>
|
||||
public async Task<AdvisoryFileUpdateResult> UpdateFileAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
string branch,
|
||||
string filePath,
|
||||
string content,
|
||||
string commitMessage,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.UpdateFileAsync(owner, repo, branch, filePath, content, commitMessage, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pull request / merge request.
|
||||
/// </summary>
|
||||
public async Task<AdvisoryPrCreateResult> CreatePullRequestAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
string headBranch,
|
||||
string baseBranch,
|
||||
string title,
|
||||
string body,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.CreatePullRequestAsync(owner, repo, headBranch, baseBranch, title, body, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets pull request details and status.
|
||||
/// </summary>
|
||||
public async Task<AdvisoryPrStatusResult> GetPullRequestStatusAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
int prNumber,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.GetPullRequestStatusAsync(owner, repo, prNumber, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets CI/CD pipeline status for a commit.
|
||||
/// </summary>
|
||||
public async Task<AdvisoryCiStatusResult> GetCiStatusAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
string commitSha,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.GetCiStatusAsync(owner, repo, commitSha, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates pull request body/description.
|
||||
/// </summary>
|
||||
public async Task<bool> UpdatePullRequestAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
int prNumber,
|
||||
string? title,
|
||||
string? body,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.UpdatePullRequestAsync(owner, repo, prNumber, title, body, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a comment to a pull request.
|
||||
/// </summary>
|
||||
public async Task<bool> AddCommentAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
int prNumber,
|
||||
string comment,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.AddCommentAsync(owner, repo, prNumber, comment, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes a pull request without merging.
|
||||
/// </summary>
|
||||
public async Task<bool> ClosePullRequestAsync(
|
||||
string owner,
|
||||
string repo,
|
||||
int prNumber,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return await _inner.ClosePullRequestAsync(owner, repo, prNumber, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPlugin
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InitializeAsync(IPluginContext context, CancellationToken ct)
|
||||
{
|
||||
_context = context;
|
||||
_state = PluginLifecycleState.Initializing;
|
||||
|
||||
// Verify the connector is available
|
||||
if (!_plugin.IsAvailable(_options))
|
||||
{
|
||||
_state = PluginLifecycleState.Failed;
|
||||
throw new InvalidOperationException(
|
||||
$"SCM connector '{_inner.ScmType}' is not available. Check API token configuration.");
|
||||
}
|
||||
|
||||
_state = PluginLifecycleState.Active;
|
||||
context.Logger.Info("SCM plugin adapter initialized for {ScmType}", _inner.ScmType);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<HealthCheckResult> HealthCheckAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var testResult = await TestConnectionAsync(ct);
|
||||
|
||||
if (testResult.Success)
|
||||
{
|
||||
return HealthCheckResult.Healthy()
|
||||
.WithDetails(new Dictionary<string, object>
|
||||
{
|
||||
["scmType"] = _inner.ScmType,
|
||||
["displayName"] = _plugin.DisplayName,
|
||||
["latencyMs"] = testResult.Latency?.TotalMilliseconds ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
return HealthCheckResult.Unhealthy(testResult.Message ?? "Connection test failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
_state = PluginLifecycleState.Stopped;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
namespace StellaOps.AdvisoryAI.Scm.Plugin.Unified;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.AdvisoryAI.Remediation.ScmConnector;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Capabilities;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating unified SCM plugin adapters from existing connectors.
|
||||
/// </summary>
|
||||
public sealed class ScmPluginAdapterFactory
|
||||
{
|
||||
private readonly ScmConnectorCatalog _catalog;
|
||||
private readonly Dictionary<string, ScmPluginAdapter> _adapters = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new factory instance.
|
||||
/// </summary>
|
||||
/// <param name="catalog">The SCM connector catalog.</param>
|
||||
public ScmPluginAdapterFactory(ScmConnectorCatalog catalog)
|
||||
{
|
||||
_catalog = catalog ?? throw new ArgumentNullException(nameof(catalog));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available unified SCM plugins.
|
||||
/// </summary>
|
||||
/// <param name="options">Connector options.</param>
|
||||
/// <returns>List of unified SCM plugins.</returns>
|
||||
public IReadOnlyList<IPlugin> GetAllPlugins(ScmConnectorOptions options)
|
||||
{
|
||||
var result = new List<IPlugin>();
|
||||
|
||||
foreach (var plugin in _catalog.Plugins)
|
||||
{
|
||||
if (plugin.IsAvailable(options))
|
||||
{
|
||||
var adapter = GetOrCreateAdapter(plugin.ScmType, options);
|
||||
if (adapter != null)
|
||||
{
|
||||
result.Add(adapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unified SCM plugin by SCM type.
|
||||
/// </summary>
|
||||
/// <param name="scmType">SCM type identifier.</param>
|
||||
/// <param name="options">Connector options.</param>
|
||||
/// <returns>Unified SCM plugin, or null if not found.</returns>
|
||||
public IPlugin? GetPlugin(string scmType, ScmConnectorOptions options)
|
||||
{
|
||||
return GetOrCreateAdapter(scmType, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a unified SCM plugin that can handle the given repository URL.
|
||||
/// </summary>
|
||||
/// <param name="repositoryUrl">Repository URL to handle.</param>
|
||||
/// <param name="options">Connector options.</param>
|
||||
/// <returns>Unified SCM plugin, or null if not found.</returns>
|
||||
public IPlugin? GetPluginForRepository(string repositoryUrl, ScmConnectorOptions options)
|
||||
{
|
||||
var plugin = _catalog.Plugins.FirstOrDefault(p => p.CanHandle(repositoryUrl));
|
||||
if (plugin == null)
|
||||
return null;
|
||||
|
||||
return GetOrCreateAdapter(plugin.ScmType, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SCM capability for a connector.
|
||||
/// </summary>
|
||||
/// <param name="scmType">SCM type identifier.</param>
|
||||
/// <param name="options">Connector options.</param>
|
||||
/// <returns>SCM capability, or null if not found.</returns>
|
||||
public IScmCapability? GetCapability(string scmType, ScmConnectorOptions options)
|
||||
{
|
||||
return GetOrCreateAdapter(scmType, options);
|
||||
}
|
||||
|
||||
private ScmPluginAdapter? GetOrCreateAdapter(string scmType, ScmConnectorOptions options)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_adapters.TryGetValue(scmType, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var plugin = _catalog.Plugins.FirstOrDefault(
|
||||
p => p.ScmType.Equals(scmType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (plugin == null || !plugin.IsAvailable(options))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var connector = _catalog.GetConnector(scmType, options);
|
||||
if (connector == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var adapter = new ScmPluginAdapter(connector, plugin, options);
|
||||
_adapters[scmType] = adapter;
|
||||
|
||||
return adapter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering unified SCM plugin services.
|
||||
/// </summary>
|
||||
public static class ScmPluginAdapterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds unified SCM plugin adapter services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">Service collection.</param>
|
||||
/// <returns>Service collection for chaining.</returns>
|
||||
public static IServiceCollection AddUnifiedScmPlugins(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ScmPluginAdapterFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Description>Unified plugin adapter for AdvisoryAI SCM connectors</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
|
||||
<ProjectReference Include="..\..\Plugin\StellaOps.Plugin.Abstractions\StellaOps.Plugin.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Immutable;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Evidence.Pack;
|
||||
using StellaOps.Evidence.Pack.Models;
|
||||
|
||||
@@ -87,6 +88,8 @@ public static class EvidencePackEndpoints
|
||||
private static async Task<IResult> HandleCreateEvidencePack(
|
||||
CreateEvidencePackRequest request,
|
||||
IEvidencePackService evidencePackService,
|
||||
TimeProvider timeProvider,
|
||||
IGuidProvider guidProvider,
|
||||
HttpContext httpContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -108,7 +111,7 @@ public static class EvidencePackEndpoints
|
||||
|
||||
var claims = request.Claims.Select(c => new EvidenceClaim
|
||||
{
|
||||
ClaimId = c.ClaimId ?? $"claim-{Guid.NewGuid():N}"[..16],
|
||||
ClaimId = c.ClaimId ?? $"claim-{guidProvider.NewGuid():N}"[..16],
|
||||
Text = c.Text,
|
||||
Type = Enum.TryParse<ClaimType>(c.Type, true, out var ct) ? ct : ClaimType.Custom,
|
||||
Status = c.Status,
|
||||
@@ -119,11 +122,11 @@ public static class EvidencePackEndpoints
|
||||
|
||||
var evidence = request.Evidence.Select(e => new EvidenceItem
|
||||
{
|
||||
EvidenceId = e.EvidenceId ?? $"ev-{Guid.NewGuid():N}"[..12],
|
||||
EvidenceId = e.EvidenceId ?? $"ev-{guidProvider.NewGuid():N}"[..12],
|
||||
Type = Enum.TryParse<EvidenceType>(e.Type, true, out var et) ? et : EvidenceType.Custom,
|
||||
Uri = e.Uri,
|
||||
Digest = e.Digest ?? "sha256:unknown",
|
||||
CollectedAt = e.CollectedAt ?? DateTimeOffset.UtcNow,
|
||||
CollectedAt = e.CollectedAt ?? timeProvider.GetUtcNow(),
|
||||
Snapshot = EvidenceSnapshot.Custom(e.SnapshotType ?? "custom", (e.SnapshotData ?? new Dictionary<string, object>()).ToImmutableDictionary(x => x.Key, x => (object?)x.Value))
|
||||
}).ToArray();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.AdvisoryAI.Runs;
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
|
||||
|
||||
@@ -407,6 +408,8 @@ public static class RunEndpoints
|
||||
string runId,
|
||||
[FromBody] AddArtifactRequestDto request,
|
||||
[FromServices] IRunService runService,
|
||||
[FromServices] TimeProvider timeProvider,
|
||||
[FromServices] IGuidProvider guidProvider,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
@@ -416,11 +419,11 @@ public static class RunEndpoints
|
||||
{
|
||||
var run = await runService.AddArtifactAsync(tenantId, runId, new RunArtifact
|
||||
{
|
||||
ArtifactId = request.ArtifactId ?? Guid.NewGuid().ToString("N"),
|
||||
ArtifactId = request.ArtifactId ?? guidProvider.NewGuid().ToString("N"),
|
||||
Type = request.Type,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
CreatedAt = timeProvider.GetUtcNow(),
|
||||
ContentDigest = request.ContentDigest,
|
||||
ContentSize = request.ContentSize,
|
||||
MediaType = request.MediaType,
|
||||
|
||||
@@ -55,6 +55,12 @@ public sealed class InMemoryAiConsentStore : IAiConsentStore
|
||||
{
|
||||
private readonly Dictionary<string, AiConsentRecord> _consents = new();
|
||||
private readonly object _lock = new();
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public InMemoryAiConsentStore(TimeProvider? timeProvider = null)
|
||||
{
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public Task<AiConsentRecord?> GetConsentAsync(string tenantId, string userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -64,7 +70,7 @@ public sealed class InMemoryAiConsentStore : IAiConsentStore
|
||||
if (_consents.TryGetValue(key, out var record))
|
||||
{
|
||||
// Check expiration
|
||||
if (record.ExpiresAt.HasValue && record.ExpiresAt.Value < DateTimeOffset.UtcNow)
|
||||
if (record.ExpiresAt.HasValue && record.ExpiresAt.Value < _timeProvider.GetUtcNow())
|
||||
{
|
||||
_consents.Remove(key);
|
||||
return Task.FromResult<AiConsentRecord?>(null);
|
||||
@@ -78,7 +84,7 @@ public sealed class InMemoryAiConsentStore : IAiConsentStore
|
||||
public Task<AiConsentRecord> GrantConsentAsync(string tenantId, string userId, AiConsentGrant grant, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = MakeKey(tenantId, userId);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var record = new AiConsentRecord
|
||||
{
|
||||
TenantId = tenantId,
|
||||
|
||||
@@ -53,11 +53,18 @@ public sealed record AiJustificationResult
|
||||
public sealed class DefaultAiJustificationGenerator : IAiJustificationGenerator
|
||||
{
|
||||
private readonly ILogger<DefaultAiJustificationGenerator> _logger;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly StellaOps.Determinism.IGuidProvider _guidProvider;
|
||||
private const string ModelVersion = "advisory-ai-v1.2.0";
|
||||
|
||||
public DefaultAiJustificationGenerator(ILogger<DefaultAiJustificationGenerator> logger)
|
||||
public DefaultAiJustificationGenerator(
|
||||
ILogger<DefaultAiJustificationGenerator> logger,
|
||||
TimeProvider? timeProvider = null,
|
||||
StellaOps.Determinism.IGuidProvider? guidProvider = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? StellaOps.Determinism.SystemGuidProvider.Instance;
|
||||
}
|
||||
|
||||
public Task<AiJustificationResult> GenerateAsync(AiJustificationRequest request, CancellationToken cancellationToken = default)
|
||||
@@ -74,13 +81,13 @@ public sealed class DefaultAiJustificationGenerator : IAiJustificationGenerator
|
||||
|
||||
var result = new AiJustificationResult
|
||||
{
|
||||
JustificationId = $"justify-{Guid.NewGuid():N}",
|
||||
JustificationId = $"justify-{_guidProvider.NewGuid():N}",
|
||||
DraftJustification = justification,
|
||||
SuggestedJustificationType = suggestedType,
|
||||
ConfidenceScore = confidence,
|
||||
EvidenceSuggestions = evidenceSuggestions,
|
||||
ModelVersion = ModelVersion,
|
||||
GeneratedAt = DateTimeOffset.UtcNow,
|
||||
GeneratedAt = _timeProvider.GetUtcNow(),
|
||||
TraceId = request.CorrelationId
|
||||
};
|
||||
|
||||
|
||||
@@ -17,5 +17,7 @@
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.AdvisoryAI.Attestation\StellaOps.AdvisoryAI.Attestation.csproj" />
|
||||
<!-- Evidence Packs (Sprint: SPRINT_20260109_011_005) -->
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Evidence.Pack\StellaOps.Evidence.Pack.csproj" />
|
||||
<!-- Determinism abstractions -->
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -168,17 +168,19 @@ public sealed record ReplayVerificationResult
|
||||
public sealed class AIArtifactReplayer : IAIArtifactReplayer
|
||||
{
|
||||
private readonly ILlmProvider _provider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public AIArtifactReplayer(ILlmProvider provider)
|
||||
public AIArtifactReplayer(ILlmProvider provider, TimeProvider? timeProvider = null)
|
||||
{
|
||||
_provider = provider;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<ReplayResult> ReplayAsync(
|
||||
AIArtifactReplayManifest manifest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var startTime = DateTime.UtcNow;
|
||||
var startTime = _timeProvider.GetUtcNow();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -191,7 +193,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
|
||||
ReplayedOutput = string.Empty,
|
||||
ReplayedOutputHash = string.Empty,
|
||||
Identical = false,
|
||||
Duration = DateTime.UtcNow - startTime,
|
||||
Duration = _timeProvider.GetUtcNow() - startTime,
|
||||
ErrorMessage = "Replay requires temperature=0 for determinism"
|
||||
};
|
||||
}
|
||||
@@ -205,7 +207,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
|
||||
ReplayedOutput = string.Empty,
|
||||
ReplayedOutputHash = string.Empty,
|
||||
Identical = false,
|
||||
Duration = DateTime.UtcNow - startTime,
|
||||
Duration = _timeProvider.GetUtcNow() - startTime,
|
||||
ErrorMessage = $"Model {manifest.ModelId} is not available"
|
||||
};
|
||||
}
|
||||
@@ -233,7 +235,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
|
||||
ReplayedOutput = result.Content,
|
||||
ReplayedOutputHash = replayedHash,
|
||||
Identical = identical,
|
||||
Duration = DateTime.UtcNow - startTime
|
||||
Duration = _timeProvider.GetUtcNow() - startTime
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -244,7 +246,7 @@ public sealed class AIArtifactReplayer : IAIArtifactReplayer
|
||||
ReplayedOutput = string.Empty,
|
||||
ReplayedOutputHash = string.Empty,
|
||||
Identical = false,
|
||||
Duration = DateTime.UtcNow - startTime,
|
||||
Duration = _timeProvider.GetUtcNow() - startTime,
|
||||
ErrorMessage = ex.Message
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public sealed class ConversationStore : IConversationStore, IAsyncDisposable
|
||||
private readonly NpgsqlDataSource _dataSource;
|
||||
private readonly ILogger<ConversationStore> _logger;
|
||||
private readonly ConversationStoreOptions _options;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@@ -32,11 +33,13 @@ public sealed class ConversationStore : IConversationStore, IAsyncDisposable
|
||||
public ConversationStore(
|
||||
NpgsqlDataSource dataSource,
|
||||
ILogger<ConversationStore> logger,
|
||||
ConversationStoreOptions? options = null)
|
||||
ConversationStoreOptions? options = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_logger = logger;
|
||||
_options = options ?? new ConversationStoreOptions();
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -217,7 +220,7 @@ public sealed class ConversationStore : IConversationStore, IAsyncDisposable
|
||||
WHERE updated_at < @cutoff
|
||||
""";
|
||||
|
||||
var cutoff = DateTimeOffset.UtcNow - maxAge;
|
||||
var cutoff = _timeProvider.GetUtcNow() - maxAge;
|
||||
|
||||
await using var cmd = _dataSource.CreateCommand(sql);
|
||||
cmd.Parameters.AddWithValue("cutoff", cutoff);
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user