Auto-rebuild AdvisoryAI knowledge corpus on startup

This commit is contained in:
master
2026-03-10 20:18:12 +02:00
parent d93006a8fa
commit f727ec24fd
7 changed files with 435 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ using StellaOps.AdvisoryAI.Evidence;
using StellaOps.AdvisoryAI.Explanation;
using StellaOps.AdvisoryAI.Hosting;
using StellaOps.AdvisoryAI.Inference.LlmProviders;
using StellaOps.AdvisoryAI.KnowledgeSearch;
using StellaOps.AdvisoryAI.Metrics;
using StellaOps.AdvisoryAI.Orchestration;
using StellaOps.AdvisoryAI.Outputs;
@@ -53,6 +54,7 @@ builder.Configuration
builder.Services.AddAdvisoryAiCore(builder.Configuration);
builder.Services.AddUnifiedSearch(builder.Configuration);
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, KnowledgeSearchStartupRebuildService>());
var llmAdapterEnabled = builder.Configuration.GetValue<bool?>("AdvisoryAI:Adapters:Llm:Enabled") ?? false;
if (llmAdapterEnabled)

View File

@@ -54,6 +54,8 @@ public sealed class KnowledgeSearchOptions
public List<string> OpenApiRoots { get; set; } = ["src", "devops/compose"];
public bool KnowledgeAutoIndexOnStartup { get; set; } = true;
public string UnifiedFindingsSnapshotPath { get; set; } =
"src/AdvisoryAI/StellaOps.AdvisoryAI/UnifiedSearch/Snapshots/findings.snapshot.json";

View File

@@ -0,0 +1,57 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.AdvisoryAI.KnowledgeSearch;
internal sealed class KnowledgeSearchStartupRebuildService : IHostedService
{
private readonly KnowledgeSearchOptions _options;
private readonly IKnowledgeIndexer _indexer;
private readonly ILogger<KnowledgeSearchStartupRebuildService> _logger;
public KnowledgeSearchStartupRebuildService(
IOptions<KnowledgeSearchOptions> options,
IKnowledgeIndexer indexer,
ILogger<KnowledgeSearchStartupRebuildService> logger)
{
ArgumentNullException.ThrowIfNull(options);
_options = options.Value ?? new KnowledgeSearchOptions();
_indexer = indexer ?? throw new ArgumentNullException(nameof(indexer));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_options.Enabled)
{
_logger.LogDebug("AdvisoryAI knowledge search is disabled; skipping startup rebuild.");
return;
}
if (!_options.KnowledgeAutoIndexOnStartup)
{
_logger.LogDebug("AdvisoryAI knowledge startup rebuild is disabled.");
return;
}
try
{
var summary = await _indexer.RebuildAsync(cancellationToken).ConfigureAwait(false);
_logger.LogInformation(
"AdvisoryAI knowledge startup rebuild completed: documents={DocumentCount}, chunks={ChunkCount}, api_specs={ApiSpecCount}, api_operations={ApiOperationCount}, doctor_projections={DoctorProjectionCount}, duration_ms={DurationMs}",
summary.DocumentCount,
summary.ChunkCount,
summary.ApiSpecCount,
summary.ApiOperationCount,
summary.DoctorProjectionCount,
summary.DurationMs);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "AdvisoryAI knowledge startup rebuild failed.");
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -0,0 +1,81 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.AdvisoryAI.KnowledgeSearch;
using Xunit;
namespace StellaOps.AdvisoryAI.Tests.KnowledgeSearch;
[Trait("Category", "Unit")]
public sealed class KnowledgeSearchStartupRebuildServiceTests
{
[Fact]
public async Task StartAsync_rebuilds_knowledge_index_when_enabled()
{
var indexer = new RecordingKnowledgeIndexer();
var service = new KnowledgeSearchStartupRebuildService(
Options.Create(new KnowledgeSearchOptions
{
Enabled = true,
KnowledgeAutoIndexOnStartup = true,
}),
indexer,
NullLogger<KnowledgeSearchStartupRebuildService>.Instance);
await service.StartAsync(CancellationToken.None);
Assert.Equal(1, indexer.RebuildCallCount);
}
[Fact]
public async Task StartAsync_skips_rebuild_when_startup_bootstrap_is_disabled()
{
var indexer = new RecordingKnowledgeIndexer();
var service = new KnowledgeSearchStartupRebuildService(
Options.Create(new KnowledgeSearchOptions
{
Enabled = true,
KnowledgeAutoIndexOnStartup = false,
}),
indexer,
NullLogger<KnowledgeSearchStartupRebuildService>.Instance);
await service.StartAsync(CancellationToken.None);
Assert.Equal(0, indexer.RebuildCallCount);
}
[Fact]
public async Task StartAsync_skips_rebuild_when_knowledge_search_is_disabled()
{
var indexer = new RecordingKnowledgeIndexer();
var service = new KnowledgeSearchStartupRebuildService(
Options.Create(new KnowledgeSearchOptions
{
Enabled = false,
KnowledgeAutoIndexOnStartup = true,
}),
indexer,
NullLogger<KnowledgeSearchStartupRebuildService>.Instance);
await service.StartAsync(CancellationToken.None);
Assert.Equal(0, indexer.RebuildCallCount);
}
private sealed class RecordingKnowledgeIndexer : IKnowledgeIndexer
{
public int RebuildCallCount { get; private set; }
public Task<KnowledgeRebuildSummary> RebuildAsync(CancellationToken cancellationToken)
{
RebuildCallCount++;
return Task.FromResult(new KnowledgeRebuildSummary(
DocumentCount: 470,
ChunkCount: 9050,
ApiSpecCount: 1,
ApiOperationCount: 2190,
DoctorProjectionCount: 8,
DurationMs: 42));
}
}
}