Add dummy LLM provider, update Concelier sources and JobEngine endpoints

- AdvisoryAI: DummyLlmProvider for offline/testing scenarios,
  wire in LlmProviderFactory
- Concelier: source definitions, registry, and management endpoint updates
- JobEngine: approval and release endpoint updates
- etc/llm-providers/dummy.yaml config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-30 17:25:48 +03:00
parent a6ffb38ecf
commit 260fce8ef8
8 changed files with 342 additions and 55 deletions

View File

@@ -1,6 +1,7 @@
using HttpResults = Microsoft.AspNetCore.Http.Results;
using Microsoft.AspNetCore.Mvc;
using StellaOps.Auth.ServerIntegration.Tenancy;
using StellaOps.Concelier.Core.Jobs;
using StellaOps.Concelier.Core.Sources;
namespace StellaOps.Concelier.WebService.Extensions;
@@ -220,6 +221,69 @@ internal static class SourceManagementEndpointExtensions
.Produces(StatusCodes.Status400BadRequest)
.RequireAuthorization(SourcesManagePolicy);
// POST /{sourceId}/sync — trigger data sync for a single source
group.MapPost("/{sourceId}/sync", async (
string sourceId,
[FromServices] ISourceRegistry registry,
[FromServices] IJobCoordinator coordinator,
CancellationToken cancellationToken) =>
{
var source = registry.GetSource(sourceId);
if (source is null)
{
return HttpResults.NotFound(new { error = "source_not_found", sourceId });
}
var fetchKind = $"source:{sourceId}:fetch";
var result = await coordinator.TriggerAsync(fetchKind, null, "manual", cancellationToken).ConfigureAwait(false);
return result.Outcome switch
{
JobTriggerOutcome.Accepted => HttpResults.Accepted(null as string, new { sourceId, jobKind = fetchKind, outcome = "accepted", runId = result.Run?.RunId }),
JobTriggerOutcome.AlreadyRunning => HttpResults.Conflict(new { sourceId, jobKind = fetchKind, outcome = "already_running" }),
JobTriggerOutcome.NotFound => HttpResults.Ok(new { sourceId, jobKind = fetchKind, outcome = "no_job_defined", message = $"No fetch job registered for source '{sourceId}'" }),
_ => HttpResults.UnprocessableEntity(new { sourceId, jobKind = fetchKind, outcome = result.Outcome.ToString().ToLowerInvariant(), error = result.ErrorMessage })
};
})
.WithName("SyncSource")
.WithSummary("Trigger data sync for a single advisory source")
.WithDescription("Immediately triggers the fetch job for the specified source. Returns 202 Accepted with the job run ID, or 409 Conflict if already running.")
.Produces(StatusCodes.Status202Accepted)
.Produces(StatusCodes.Status404NotFound)
.Produces(StatusCodes.Status409Conflict)
.RequireAuthorization(SourcesManagePolicy);
// POST /sync — trigger data sync for all enabled sources
group.MapPost("/sync", async (
[FromServices] ISourceRegistry registry,
[FromServices] IJobCoordinator coordinator,
CancellationToken cancellationToken) =>
{
var enabledIds = await registry.GetEnabledSourcesAsync(cancellationToken).ConfigureAwait(false);
var results = new List<object>(enabledIds.Length);
foreach (var sourceId in enabledIds)
{
var fetchKind = $"source:{sourceId}:fetch";
var result = await coordinator.TriggerAsync(fetchKind, null, "manual-all", cancellationToken).ConfigureAwait(false);
results.Add(new
{
sourceId,
jobKind = fetchKind,
outcome = result.Outcome.ToString().ToLowerInvariant(),
runId = result.Run?.RunId
});
}
var accepted = results.Count(r => ((dynamic)r).outcome == "accepted");
return HttpResults.Ok(new { totalTriggered = accepted, totalSources = enabledIds.Length, results });
})
.WithName("SyncAllSources")
.WithSummary("Trigger data sync for all enabled advisory sources")
.WithDescription("Immediately triggers fetch jobs for every enabled source. Returns per-source trigger results.")
.Produces(StatusCodes.Status200OK)
.RequireAuthorization(SourcesManagePolicy);
// GET /{sourceId}/check-result — get last check result for a source
group.MapGet("/{sourceId}/check-result", (
string sourceId,