save progress

This commit is contained in:
master
2026-01-09 18:27:36 +02:00
parent e608752924
commit a21d3dbc1f
361 changed files with 63068 additions and 1192 deletions

View File

@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Net;
@@ -10,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using StellaOps.AdvisoryAI.Caching;
using StellaOps.AdvisoryAI.Chat;
using StellaOps.AdvisoryAI.Diagnostics;
using StellaOps.AdvisoryAI.Explanation;
using StellaOps.AdvisoryAI.Hosting;
@@ -161,6 +163,22 @@ app.MapPost("/v1/advisory-ai/remediate", HandleRemediate)
app.MapGet("/v1/advisory-ai/rate-limits", HandleGetRateLimits)
.RequireRateLimiting("advisory-ai");
// Chat endpoints (SPRINT_20260107_006_003 CH-005)
app.MapPost("/v1/advisory-ai/conversations", HandleCreateConversation)
.RequireRateLimiting("advisory-ai");
app.MapGet("/v1/advisory-ai/conversations/{conversationId}", HandleGetConversation)
.RequireRateLimiting("advisory-ai");
app.MapPost("/v1/advisory-ai/conversations/{conversationId}/turns", HandleAddTurn)
.RequireRateLimiting("advisory-ai");
app.MapDelete("/v1/advisory-ai/conversations/{conversationId}", HandleDeleteConversation)
.RequireRateLimiting("advisory-ai");
app.MapGet("/v1/advisory-ai/conversations", HandleListConversations)
.RequireRateLimiting("advisory-ai");
// Refresh Router endpoint cache
app.TryRefreshStellaRouterEndpoints(routerOptions);
@@ -926,6 +944,245 @@ static Task<IResult> HandleGetRateLimits(
return Task.FromResult(Results.Ok(response));
}
// Chat endpoint handlers (SPRINT_20260107_006_003 CH-005)
static async Task<IResult> HandleCreateConversation(
HttpContext httpContext,
StellaOps.AdvisoryAI.WebService.Contracts.CreateConversationRequest request,
IConversationService conversationService,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.create_conversation", ActivityKind.Server);
activity?.SetTag("advisory.tenant_id", request.TenantId);
if (!EnsureChatAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
// Get user ID from header
var userId = httpContext.Request.Headers.TryGetValue("X-StellaOps-User", out var userHeader)
? userHeader.ToString()
: "anonymous";
var conversationRequest = new ConversationRequest
{
TenantId = request.TenantId,
UserId = userId,
InitialContext = request.Context is not null
? new ConversationContext
{
CurrentCveId = request.Context.CurrentCveId,
CurrentComponent = request.Context.CurrentComponent,
CurrentImageDigest = request.Context.CurrentImageDigest,
ScanId = request.Context.ScanId,
SbomId = request.Context.SbomId
}
: null,
Metadata = request.Metadata?.ToImmutableDictionary()
};
var conversation = await conversationService.CreateAsync(conversationRequest, cancellationToken).ConfigureAwait(false);
activity?.SetTag("advisory.conversation_id", conversation.ConversationId);
return Results.Created(
$"/v1/advisory-ai/conversations/{conversation.ConversationId}",
StellaOps.AdvisoryAI.WebService.Contracts.ConversationResponse.FromConversation(conversation));
}
static async Task<IResult> HandleGetConversation(
HttpContext httpContext,
string conversationId,
IConversationService conversationService,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.get_conversation", ActivityKind.Server);
activity?.SetTag("advisory.conversation_id", conversationId);
if (!EnsureChatAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
var conversation = await conversationService.GetAsync(conversationId, cancellationToken).ConfigureAwait(false);
if (conversation is null)
{
return Results.NotFound(new { error = $"Conversation '{conversationId}' not found" });
}
return Results.Ok(StellaOps.AdvisoryAI.WebService.Contracts.ConversationResponse.FromConversation(conversation));
}
static async Task<IResult> HandleAddTurn(
HttpContext httpContext,
string conversationId,
StellaOps.AdvisoryAI.WebService.Contracts.AddTurnRequest request,
IConversationService conversationService,
ChatPromptAssembler? promptAssembler,
ChatResponseStreamer? responseStreamer,
GroundingValidator? groundingValidator,
ActionProposalParser? actionParser,
TimeProvider timeProvider,
ILogger<Program> logger,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.add_turn", ActivityKind.Server);
activity?.SetTag("advisory.conversation_id", conversationId);
activity?.SetTag("advisory.stream", request.Stream);
if (!EnsureChatAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
var startTime = timeProvider.GetUtcNow();
// Add user turn
try
{
var userTurnRequest = new TurnRequest
{
Role = TurnRole.User,
Content = request.Content,
Metadata = request.Metadata?.ToImmutableDictionary()
};
var userTurn = await conversationService.AddTurnAsync(conversationId, userTurnRequest, cancellationToken)
.ConfigureAwait(false);
activity?.SetTag("advisory.user_turn_id", userTurn.TurnId);
// For now, return a placeholder response since we don't have the full LLM pipeline
// In a complete implementation, this would call the prompt assembler, LLM, and validators
var assistantContent = GeneratePlaceholderResponse(request.Content);
var assistantTurnRequest = new TurnRequest
{
Role = TurnRole.Assistant,
Content = assistantContent
};
var assistantTurn = await conversationService.AddTurnAsync(conversationId, assistantTurnRequest, cancellationToken)
.ConfigureAwait(false);
var elapsed = timeProvider.GetUtcNow() - startTime;
var response = new StellaOps.AdvisoryAI.WebService.Contracts.AssistantTurnResponse
{
TurnId = assistantTurn.TurnId,
Content = assistantTurn.Content,
Timestamp = assistantTurn.Timestamp,
EvidenceLinks = assistantTurn.EvidenceLinks.IsEmpty
? null
: assistantTurn.EvidenceLinks.Select(StellaOps.AdvisoryAI.WebService.Contracts.EvidenceLinkResponse.FromLink).ToList(),
ProposedActions = assistantTurn.ProposedActions.IsEmpty
? null
: assistantTurn.ProposedActions.Select(StellaOps.AdvisoryAI.WebService.Contracts.ProposedActionResponse.FromAction).ToList(),
GroundingScore = 1.0, // Placeholder
TokenCount = assistantContent.Split(' ').Length, // Rough estimate
DurationMs = (long)elapsed.TotalMilliseconds
};
return Results.Ok(response);
}
catch (ConversationNotFoundException)
{
return Results.NotFound(new { error = $"Conversation '{conversationId}' not found" });
}
}
static async Task<IResult> HandleDeleteConversation(
HttpContext httpContext,
string conversationId,
IConversationService conversationService,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.delete_conversation", ActivityKind.Server);
activity?.SetTag("advisory.conversation_id", conversationId);
if (!EnsureChatAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
var deleted = await conversationService.DeleteAsync(conversationId, cancellationToken).ConfigureAwait(false);
if (!deleted)
{
return Results.NotFound(new { error = $"Conversation '{conversationId}' not found" });
}
return Results.NoContent();
}
static async Task<IResult> HandleListConversations(
HttpContext httpContext,
string? tenantId,
int? limit,
IConversationService conversationService,
CancellationToken cancellationToken)
{
using var activity = AdvisoryAiActivitySource.Instance.StartActivity("advisory_ai.list_conversations", ActivityKind.Server);
if (!EnsureChatAuthorized(httpContext))
{
return Results.StatusCode(StatusCodes.Status403Forbidden);
}
// Get tenant from header if not provided
var effectiveTenantId = tenantId
?? (httpContext.Request.Headers.TryGetValue("X-StellaOps-Tenant", out var tenantHeader)
? tenantHeader.ToString()
: "default");
// Get user from header for filtering
var userId = httpContext.Request.Headers.TryGetValue("X-StellaOps-User", out var userHeader)
? userHeader.ToString()
: null;
var conversations = await conversationService.ListAsync(effectiveTenantId, userId, limit, cancellationToken)
.ConfigureAwait(false);
var summaries = conversations.Select(c => new StellaOps.AdvisoryAI.WebService.Contracts.ConversationSummary
{
ConversationId = c.ConversationId,
CreatedAt = c.CreatedAt,
UpdatedAt = c.UpdatedAt,
TurnCount = c.Turns.Length,
Preview = c.Turns.FirstOrDefault(t => t.Role == TurnRole.User)?.Content is { } content
? content.Length > 100 ? content[..100] + "..." : content
: null
}).ToList();
return Results.Ok(new StellaOps.AdvisoryAI.WebService.Contracts.ConversationListResponse
{
Conversations = summaries,
TotalCount = summaries.Count
});
}
static bool EnsureChatAuthorized(HttpContext context)
{
if (!context.Request.Headers.TryGetValue("X-StellaOps-Scopes", out var scopes))
{
return false;
}
var allowed = scopes
.SelectMany(value => value?.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? [])
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return allowed.Contains("advisory:run") || allowed.Contains("advisory:chat");
}
static string GeneratePlaceholderResponse(string userMessage)
{
// Placeholder implementation - in production this would call the LLM
return $"I received your message: \"{userMessage}\". This is a placeholder response. " +
"The full chat functionality with grounded responses will be implemented when the LLM pipeline is connected.";
}
internal sealed record PipelinePlanRequest(
AdvisoryTaskType? TaskType,
string AdvisoryKey,