Complete batch 012 (golden set diff) and 013 (advisory chat), fix build errors
Sprints completed: - SPRINT_20260110_012_* (golden set diff layer - 10 sprints) - SPRINT_20260110_013_* (advisory chat - 4 sprints) Build fixes applied: - Fix namespace conflicts with Microsoft.Extensions.Options.Options.Create - Fix VexDecisionReachabilityIntegrationTests API drift (major rewrite) - Fix VexSchemaValidationTests FluentAssertions method name - Fix FixChainGateIntegrationTests ambiguous type references - Fix AdvisoryAI test files required properties and namespace aliases - Add stub types for CveMappingController (ICveSymbolMappingService) - Fix VerdictBuilderService static context issue Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,752 @@
|
||||
// <copyright file="ChatEndpoints.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.AdvisoryAI.Chat.Assembly;
|
||||
using StellaOps.AdvisoryAI.Chat.Inference;
|
||||
using StellaOps.AdvisoryAI.Chat.Models;
|
||||
using StellaOps.AdvisoryAI.Chat.Options;
|
||||
using StellaOps.AdvisoryAI.Chat.Routing;
|
||||
using StellaOps.AdvisoryAI.Chat.Services;
|
||||
using StellaOps.AdvisoryAI.WebService.Contracts;
|
||||
|
||||
namespace StellaOps.AdvisoryAI.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// API endpoints for Advisory AI Chat with streaming support.
|
||||
/// Sprint: SPRINT_20260107_013_003 Task: SVC-003
|
||||
/// </summary>
|
||||
public static class ChatEndpoints
|
||||
{
|
||||
private static readonly JsonSerializerOptions StreamJsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maps chat endpoints to the route builder.
|
||||
/// </summary>
|
||||
/// <param name="builder">The endpoint route builder.</param>
|
||||
/// <returns>The route group builder.</returns>
|
||||
public static RouteGroupBuilder MapChatEndpoints(this IEndpointRouteBuilder builder)
|
||||
{
|
||||
var group = builder.MapGroup("/api/v1/chat")
|
||||
.WithTags("Advisory Chat");
|
||||
|
||||
// Single query endpoint (non-streaming)
|
||||
group.MapPost("/query", ProcessQueryAsync)
|
||||
.WithName("ProcessChatQuery")
|
||||
.WithSummary("Processes a chat query and returns an evidence-grounded response")
|
||||
.WithDescription("Analyzes the user query, assembles evidence bundle, and generates a response with citations.")
|
||||
.Produces<AdvisoryChatQueryResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status503ServiceUnavailable)
|
||||
.ProducesValidationProblem();
|
||||
|
||||
// Streaming query endpoint
|
||||
group.MapPost("/query/stream", StreamQueryAsync)
|
||||
.WithName("StreamChatQuery")
|
||||
.WithSummary("Streams a chat response as Server-Sent Events")
|
||||
.WithDescription("Processes the query and streams the response tokens as SSE events.")
|
||||
.Produces(StatusCodes.Status200OK, contentType: "text/event-stream")
|
||||
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status503ServiceUnavailable);
|
||||
|
||||
// Intent detection endpoint (lightweight)
|
||||
group.MapPost("/intent", DetectIntentAsync)
|
||||
.WithName("DetectChatIntent")
|
||||
.WithSummary("Detects intent from a user query without generating a full response")
|
||||
.Produces<IntentDetectionResponse>(StatusCodes.Status200OK)
|
||||
.ProducesValidationProblem();
|
||||
|
||||
// Evidence bundle preview endpoint
|
||||
group.MapPost("/evidence-preview", PreviewEvidenceBundleAsync)
|
||||
.WithName("PreviewEvidenceBundle")
|
||||
.WithSummary("Previews the evidence bundle that would be assembled for a query")
|
||||
.Produces<EvidenceBundlePreviewResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ErrorResponse>(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Health/status endpoint for chat service
|
||||
group.MapGet("/status", GetChatStatusAsync)
|
||||
.WithName("GetChatStatus")
|
||||
.WithSummary("Gets the status of the advisory chat service")
|
||||
.Produces<ChatServiceStatusResponse>(StatusCodes.Status200OK);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static async Task<IResult> ProcessQueryAsync(
|
||||
[FromBody] AdvisoryChatQueryRequest request,
|
||||
[FromServices] IAdvisoryChatService chatService,
|
||||
[FromServices] IOptions<AdvisoryChatOptions> options,
|
||||
[FromServices] ILogger<AdvisoryChatQueryRequest> logger,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-User-Id")] string? userId,
|
||||
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!options.Value.Enabled)
|
||||
{
|
||||
return Results.Json(
|
||||
new ErrorResponse { Error = "Advisory chat is disabled", Code = "CHAT_DISABLED" },
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Query))
|
||||
{
|
||||
return Results.BadRequest(new ErrorResponse { Error = "Query cannot be empty", Code = "INVALID_QUERY" });
|
||||
}
|
||||
|
||||
tenantId ??= "default";
|
||||
userId ??= "anonymous";
|
||||
|
||||
logger.LogDebug("Processing chat query for tenant {TenantId}, user {UserId}", tenantId, userId);
|
||||
|
||||
var serviceRequest = new AdvisoryChatRequest
|
||||
{
|
||||
TenantId = tenantId,
|
||||
UserId = userId,
|
||||
Query = request.Query,
|
||||
ArtifactDigest = request.ArtifactDigest,
|
||||
ImageReference = request.ImageReference,
|
||||
Environment = request.Environment,
|
||||
CorrelationId = correlationId,
|
||||
ConversationId = request.ConversationId,
|
||||
UserRoles = request.UserRoles?.ToImmutableArray() ?? ImmutableArray<string>.Empty
|
||||
};
|
||||
|
||||
var result = await chatService.ProcessQueryAsync(serviceRequest, ct);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
var statusCode = result.GuardrailBlocked
|
||||
? StatusCodes.Status400BadRequest
|
||||
: StatusCodes.Status500InternalServerError;
|
||||
|
||||
return Results.Json(
|
||||
new ErrorResponse
|
||||
{
|
||||
Error = result.Error ?? "Query processing failed",
|
||||
Code = result.GuardrailBlocked ? "GUARDRAIL_BLOCKED" : "PROCESSING_FAILED",
|
||||
Details = result.GuardrailBlocked
|
||||
? result.GuardrailViolations.ToDictionary(v => v, _ => (object)"violation")
|
||||
: null
|
||||
},
|
||||
statusCode: statusCode);
|
||||
}
|
||||
|
||||
return Results.Ok(MapToQueryResponse(result));
|
||||
}
|
||||
|
||||
private static async Task StreamQueryAsync(
|
||||
[FromBody] AdvisoryChatQueryRequest request,
|
||||
[FromServices] IAdvisoryChatIntentRouter intentRouter,
|
||||
[FromServices] IEvidenceBundleAssembler evidenceAssembler,
|
||||
[FromServices] IAdvisoryChatInferenceClient inferenceClient,
|
||||
[FromServices] IOptions<AdvisoryChatOptions> options,
|
||||
[FromServices] ILogger<AdvisoryChatQueryRequest> logger,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-User-Id")] string? userId,
|
||||
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
|
||||
HttpContext httpContext,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!options.Value.Enabled)
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
await httpContext.Response.WriteAsJsonAsync(
|
||||
new ErrorResponse { Error = "Advisory chat is disabled", Code = "CHAT_DISABLED" },
|
||||
ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Query))
|
||||
{
|
||||
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
await httpContext.Response.WriteAsJsonAsync(
|
||||
new ErrorResponse { Error = "Query cannot be empty", Code = "INVALID_QUERY" },
|
||||
ct);
|
||||
return;
|
||||
}
|
||||
|
||||
tenantId ??= "default";
|
||||
|
||||
httpContext.Response.ContentType = "text/event-stream";
|
||||
httpContext.Response.Headers.CacheControl = "no-cache";
|
||||
httpContext.Response.Headers.Connection = "keep-alive";
|
||||
|
||||
await httpContext.Response.StartAsync(ct);
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: Route intent
|
||||
var routingResult = await intentRouter.RouteAsync(request.Query, ct);
|
||||
|
||||
await WriteStreamEventAsync(httpContext, "intent", new
|
||||
{
|
||||
intent = routingResult.Intent.ToString(),
|
||||
confidence = routingResult.Confidence,
|
||||
parameters = routingResult.Parameters
|
||||
}, ct);
|
||||
|
||||
// Step 2: Resolve context
|
||||
var artifactDigest = request.ArtifactDigest ?? ExtractDigestFromImageRef(routingResult.Parameters.ImageReference);
|
||||
var findingId = routingResult.Parameters.FindingId;
|
||||
|
||||
if (string.IsNullOrEmpty(artifactDigest) || string.IsNullOrEmpty(findingId))
|
||||
{
|
||||
await WriteStreamEventAsync(httpContext, "error", new
|
||||
{
|
||||
code = "MISSING_CONTEXT",
|
||||
message = "Missing artifact digest or finding ID"
|
||||
}, ct);
|
||||
await WriteStreamEventAsync(httpContext, "done", new { success = false }, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 3: Assemble evidence bundle
|
||||
await WriteStreamEventAsync(httpContext, "status", new { phase = "assembling_evidence" }, ct);
|
||||
|
||||
var assemblyResult = await evidenceAssembler.AssembleAsync(
|
||||
new EvidenceBundleAssemblyRequest
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ArtifactDigest = artifactDigest,
|
||||
ImageReference = request.ImageReference ?? routingResult.Parameters.ImageReference,
|
||||
Environment = request.Environment ?? "unknown",
|
||||
FindingId = findingId,
|
||||
PackagePurl = routingResult.Parameters.Package,
|
||||
CorrelationId = correlationId
|
||||
},
|
||||
ct);
|
||||
|
||||
if (!assemblyResult.Success || assemblyResult.Bundle is null)
|
||||
{
|
||||
await WriteStreamEventAsync(httpContext, "error", new
|
||||
{
|
||||
code = "EVIDENCE_ASSEMBLY_FAILED",
|
||||
message = assemblyResult.Error ?? "Failed to assemble evidence"
|
||||
}, ct);
|
||||
await WriteStreamEventAsync(httpContext, "done", new { success = false }, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
await WriteStreamEventAsync(httpContext, "evidence", new
|
||||
{
|
||||
bundleId = assemblyResult.Bundle.BundleId,
|
||||
evidenceCount = CountEvidence(assemblyResult.Bundle)
|
||||
}, ct);
|
||||
|
||||
// Step 4: Stream inference response
|
||||
await WriteStreamEventAsync(httpContext, "status", new { phase = "generating_response" }, ct);
|
||||
|
||||
await foreach (var chunk in inferenceClient.StreamResponseAsync(
|
||||
assemblyResult.Bundle,
|
||||
routingResult,
|
||||
ct))
|
||||
{
|
||||
if (chunk.IsComplete && chunk.FinalResponse is not null)
|
||||
{
|
||||
await WriteStreamEventAsync(httpContext, "complete", MapToQueryResponse(
|
||||
new AdvisoryChatServiceResult
|
||||
{
|
||||
Success = true,
|
||||
Response = chunk.FinalResponse,
|
||||
Intent = routingResult.Intent,
|
||||
EvidenceAssembled = true
|
||||
}), ct);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(chunk.Content))
|
||||
{
|
||||
await WriteStreamEventAsync(httpContext, "token", new { content = chunk.Content }, ct);
|
||||
}
|
||||
}
|
||||
|
||||
await WriteStreamEventAsync(httpContext, "done", new { success = true }, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Client disconnected, nothing to do
|
||||
logger.LogDebug("Stream cancelled by client");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error during streaming response");
|
||||
await WriteStreamEventAsync(httpContext, "error", new
|
||||
{
|
||||
code = "STREAM_ERROR",
|
||||
message = "An error occurred during streaming"
|
||||
}, ct);
|
||||
await WriteStreamEventAsync(httpContext, "done", new { success = false }, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<IResult> DetectIntentAsync(
|
||||
[FromBody] IntentDetectionRequest request,
|
||||
[FromServices] IAdvisoryChatIntentRouter intentRouter,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Query))
|
||||
{
|
||||
return Results.BadRequest(new ErrorResponse { Error = "Query cannot be empty", Code = "INVALID_QUERY" });
|
||||
}
|
||||
|
||||
var result = await intentRouter.RouteAsync(request.Query, ct);
|
||||
|
||||
return Results.Ok(new IntentDetectionResponse
|
||||
{
|
||||
Intent = result.Intent.ToString(),
|
||||
Confidence = result.Confidence,
|
||||
NormalizedInput = result.NormalizedInput,
|
||||
ExplicitSlashCommand = result.ExplicitSlashCommand,
|
||||
Parameters = new IntentParametersResponse
|
||||
{
|
||||
FindingId = result.Parameters.FindingId,
|
||||
Package = result.Parameters.Package,
|
||||
ImageReference = result.Parameters.ImageReference,
|
||||
Environment = result.Parameters.Environment,
|
||||
Duration = result.Parameters.Duration,
|
||||
Reason = result.Parameters.Reason
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<IResult> PreviewEvidenceBundleAsync(
|
||||
[FromBody] EvidencePreviewRequest request,
|
||||
[FromServices] IEvidenceBundleAssembler evidenceAssembler,
|
||||
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
||||
[FromHeader(Name = "X-Correlation-Id")] string? correlationId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.FindingId))
|
||||
{
|
||||
return Results.BadRequest(new ErrorResponse { Error = "FindingId is required", Code = "MISSING_FINDING_ID" });
|
||||
}
|
||||
|
||||
tenantId ??= "default";
|
||||
|
||||
var assemblyResult = await evidenceAssembler.AssembleAsync(
|
||||
new EvidenceBundleAssemblyRequest
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ArtifactDigest = request.ArtifactDigest ?? "unknown",
|
||||
ImageReference = request.ImageReference,
|
||||
Environment = request.Environment ?? "unknown",
|
||||
FindingId = request.FindingId,
|
||||
PackagePurl = request.PackagePurl,
|
||||
CorrelationId = correlationId
|
||||
},
|
||||
ct);
|
||||
|
||||
if (!assemblyResult.Success || assemblyResult.Bundle is null)
|
||||
{
|
||||
return Results.BadRequest(new ErrorResponse
|
||||
{
|
||||
Error = assemblyResult.Error ?? "Failed to assemble evidence",
|
||||
Code = "EVIDENCE_ASSEMBLY_FAILED"
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Ok(new EvidenceBundlePreviewResponse
|
||||
{
|
||||
BundleId = assemblyResult.Bundle.BundleId,
|
||||
FindingId = assemblyResult.Bundle.Finding?.Id,
|
||||
HasVexData = assemblyResult.Bundle.Verdicts?.Vex is not null,
|
||||
HasReachabilityData = assemblyResult.Bundle.Reachability is not null,
|
||||
HasBinaryPatchData = assemblyResult.Bundle.Reachability?.BinaryPatch is not null,
|
||||
HasProvenanceData = assemblyResult.Bundle.Provenance is not null,
|
||||
HasPolicyData = assemblyResult.Bundle.Verdicts?.Policy.Length > 0,
|
||||
HasOpsMemoryData = assemblyResult.Bundle.OpsMemory is not null,
|
||||
HasFixData = assemblyResult.Bundle.Fixes is not null,
|
||||
EvidenceSummary = new EvidenceSummary
|
||||
{
|
||||
VexStatus = assemblyResult.Bundle.Verdicts?.Vex?.Status.ToString(),
|
||||
ReachabilityStatus = assemblyResult.Bundle.Reachability?.Status.ToString(),
|
||||
BinaryPatchDetected = assemblyResult.Bundle.Reachability?.BinaryPatch?.Detected,
|
||||
PolicyDecision = assemblyResult.Bundle.Verdicts?.Policy.FirstOrDefault()?.Decision.ToString(),
|
||||
FixOptionsCount = assemblyResult.Bundle.Fixes?.Upgrade.Length ?? 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<IResult> GetChatStatusAsync(
|
||||
[FromServices] IOptions<AdvisoryChatOptions> options)
|
||||
{
|
||||
var opts = options.Value;
|
||||
|
||||
return Task.FromResult(Results.Ok(new ChatServiceStatusResponse
|
||||
{
|
||||
Enabled = opts.Enabled,
|
||||
InferenceProvider = opts.Inference.Provider.ToString(),
|
||||
InferenceModel = opts.Inference.Model,
|
||||
MaxTokens = opts.Inference.MaxTokens,
|
||||
GuardrailsEnabled = opts.Guardrails.Enabled,
|
||||
AuditEnabled = opts.Audit.Enabled
|
||||
}));
|
||||
}
|
||||
|
||||
private static async Task WriteStreamEventAsync<T>(
|
||||
HttpContext context,
|
||||
string eventType,
|
||||
T data,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(data, StreamJsonOptions);
|
||||
await context.Response.WriteAsync($"event: {eventType}\n", ct);
|
||||
await context.Response.WriteAsync($"data: {json}\n\n", ct);
|
||||
await context.Response.Body.FlushAsync(ct);
|
||||
}
|
||||
|
||||
private static string? ExtractDigestFromImageRef(string? imageRef)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imageRef))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var atIndex = imageRef.IndexOf('@');
|
||||
if (atIndex > 0 && imageRef.Length > atIndex + 1)
|
||||
{
|
||||
return imageRef[(atIndex + 1)..];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int CountEvidence(AdvisoryChatEvidenceBundle bundle)
|
||||
{
|
||||
var count = 0;
|
||||
if (bundle.Verdicts?.Vex is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (bundle.Reachability is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (bundle.Reachability?.BinaryPatch is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (bundle.Provenance is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (bundle.Verdicts?.Policy.Length > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (bundle.OpsMemory is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (bundle.Fixes is not null)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static AdvisoryChatQueryResponse MapToQueryResponse(AdvisoryChatServiceResult result)
|
||||
{
|
||||
var response = result.Response!;
|
||||
|
||||
return new AdvisoryChatQueryResponse
|
||||
{
|
||||
ResponseId = response.ResponseId,
|
||||
BundleId = response.BundleId,
|
||||
Intent = response.Intent.ToString(),
|
||||
GeneratedAt = response.GeneratedAt,
|
||||
Summary = response.Summary,
|
||||
Impact = response.Impact is not null ? new ImpactAssessmentResponse
|
||||
{
|
||||
Artifact = response.Impact.Artifact,
|
||||
Environment = response.Impact.Environment,
|
||||
AffectedComponent = response.Impact.AffectedComponent,
|
||||
AffectedVersion = response.Impact.AffectedVersion,
|
||||
Description = response.Impact.Description
|
||||
} : null,
|
||||
Reachability = response.ReachabilityAssessment is not null ? new ReachabilityAssessmentResponse
|
||||
{
|
||||
Status = response.ReachabilityAssessment.Status.ToString(),
|
||||
CallgraphPaths = response.ReachabilityAssessment.CallgraphPaths,
|
||||
PathDescription = response.ReachabilityAssessment.PathDescription,
|
||||
BinaryBackportDetected = response.ReachabilityAssessment.BinaryBackport?.Detected
|
||||
} : null,
|
||||
Mitigations = response.Mitigations.Select(m => new MitigationOptionResponse
|
||||
{
|
||||
Rank = m.Rank,
|
||||
Type = m.Type.ToString(),
|
||||
Label = m.Label,
|
||||
Description = m.Description,
|
||||
Risk = m.Risk.ToString(),
|
||||
RequiresApproval = m.RequiresApproval
|
||||
}).ToList(),
|
||||
EvidenceLinks = response.EvidenceLinks.Select(e => new EvidenceLinkResponse
|
||||
{
|
||||
Type = e.Type.ToString(),
|
||||
Uri = e.Link,
|
||||
Label = e.Description,
|
||||
Confidence = e.Confidence is not null
|
||||
? e.Confidence == ConfidenceLevel.High ? 0.9
|
||||
: e.Confidence == ConfidenceLevel.Medium ? 0.7
|
||||
: e.Confidence == ConfidenceLevel.Low ? 0.4
|
||||
: 0.2
|
||||
: null
|
||||
}).ToList(),
|
||||
Confidence = new ConfidenceResponse
|
||||
{
|
||||
Level = response.Confidence.Level.ToString(),
|
||||
Score = response.Confidence.Score
|
||||
},
|
||||
ProposedActions = response.ProposedActions.Select(a => new ProposedActionResponse
|
||||
{
|
||||
ActionType = a.ActionType.ToString(),
|
||||
Label = a.Label,
|
||||
PolicyGate = a.RiskLevel?.ToString(),
|
||||
RequiresConfirmation = a.RequiresApproval ?? false
|
||||
}).ToList(),
|
||||
FollowUp = response.FollowUp is not null ? new FollowUpResponse
|
||||
{
|
||||
SuggestedQueries = [.. response.FollowUp.SuggestedQueries],
|
||||
NextSteps = [.. response.FollowUp.NextSteps]
|
||||
} : null,
|
||||
Diagnostics = result.Diagnostics is not null ? new DiagnosticsResponse
|
||||
{
|
||||
IntentRoutingMs = result.Diagnostics.IntentRoutingMs,
|
||||
EvidenceAssemblyMs = result.Diagnostics.EvidenceAssemblyMs,
|
||||
InferenceMs = result.Diagnostics.InferenceMs,
|
||||
TotalMs = result.Diagnostics.TotalMs,
|
||||
PromptTokens = result.Diagnostics.PromptTokens,
|
||||
CompletionTokens = result.Diagnostics.CompletionTokens
|
||||
} : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#region Request/Response DTOs
|
||||
|
||||
/// <summary>
|
||||
/// Request to process a chat query.
|
||||
/// </summary>
|
||||
public sealed record AdvisoryChatQueryRequest
|
||||
{
|
||||
/// <summary>Gets the user query.</summary>
|
||||
public required string Query { get; init; }
|
||||
|
||||
/// <summary>Gets the artifact digest.</summary>
|
||||
public string? ArtifactDigest { get; init; }
|
||||
|
||||
/// <summary>Gets the image reference.</summary>
|
||||
public string? ImageReference { get; init; }
|
||||
|
||||
/// <summary>Gets the environment.</summary>
|
||||
public string? Environment { get; init; }
|
||||
|
||||
/// <summary>Gets the conversation ID for multi-turn.</summary>
|
||||
public string? ConversationId { get; init; }
|
||||
|
||||
/// <summary>Gets the user roles for policy evaluation.</summary>
|
||||
public List<string>? UserRoles { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response from a chat query.
|
||||
/// </summary>
|
||||
public sealed record AdvisoryChatQueryResponse
|
||||
{
|
||||
/// <summary>Gets the response ID.</summary>
|
||||
public required string ResponseId { get; init; }
|
||||
|
||||
/// <summary>Gets the bundle ID.</summary>
|
||||
public string? BundleId { get; init; }
|
||||
|
||||
/// <summary>Gets the detected intent.</summary>
|
||||
public required string Intent { get; init; }
|
||||
|
||||
/// <summary>Gets the generation timestamp.</summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
|
||||
/// <summary>Gets the summary.</summary>
|
||||
public required string Summary { get; init; }
|
||||
|
||||
/// <summary>Gets the impact assessment.</summary>
|
||||
public ImpactAssessmentResponse? Impact { get; init; }
|
||||
|
||||
/// <summary>Gets the reachability assessment.</summary>
|
||||
public ReachabilityAssessmentResponse? Reachability { get; init; }
|
||||
|
||||
/// <summary>Gets the mitigation options.</summary>
|
||||
public List<MitigationOptionResponse> Mitigations { get; init; } = [];
|
||||
|
||||
/// <summary>Gets the evidence links.</summary>
|
||||
public List<EvidenceLinkResponse> EvidenceLinks { get; init; } = [];
|
||||
|
||||
/// <summary>Gets the confidence assessment.</summary>
|
||||
public required ConfidenceResponse Confidence { get; init; }
|
||||
|
||||
/// <summary>Gets the proposed actions.</summary>
|
||||
public List<ProposedActionResponse> ProposedActions { get; init; } = [];
|
||||
|
||||
/// <summary>Gets the follow-up suggestions.</summary>
|
||||
public FollowUpResponse? FollowUp { get; init; }
|
||||
|
||||
/// <summary>Gets the diagnostics.</summary>
|
||||
public DiagnosticsResponse? Diagnostics { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Impact assessment response.</summary>
|
||||
public sealed record ImpactAssessmentResponse
|
||||
{
|
||||
public string? Artifact { get; init; }
|
||||
public string? Environment { get; init; }
|
||||
public string? AffectedComponent { get; init; }
|
||||
public string? AffectedVersion { get; init; }
|
||||
public string? Description { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Reachability assessment response.</summary>
|
||||
public sealed record ReachabilityAssessmentResponse
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
public int? CallgraphPaths { get; init; }
|
||||
public string? PathDescription { get; init; }
|
||||
public bool? BinaryBackportDetected { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Mitigation option response.</summary>
|
||||
public sealed record MitigationOptionResponse
|
||||
{
|
||||
public required int Rank { get; init; }
|
||||
public required string Type { get; init; }
|
||||
public required string Label { get; init; }
|
||||
public string? Description { get; init; }
|
||||
public required string Risk { get; init; }
|
||||
public bool? RequiresApproval { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Confidence response.</summary>
|
||||
public sealed record ConfidenceResponse
|
||||
{
|
||||
public required string Level { get; init; }
|
||||
public required double Score { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Follow-up suggestions response.</summary>
|
||||
public sealed record FollowUpResponse
|
||||
{
|
||||
public List<string> SuggestedQueries { get; init; } = [];
|
||||
public List<string> NextSteps { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Diagnostics response.</summary>
|
||||
public sealed record DiagnosticsResponse
|
||||
{
|
||||
public long IntentRoutingMs { get; init; }
|
||||
public long EvidenceAssemblyMs { get; init; }
|
||||
public long InferenceMs { get; init; }
|
||||
public long TotalMs { get; init; }
|
||||
public int PromptTokens { get; init; }
|
||||
public int CompletionTokens { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Request for intent detection.</summary>
|
||||
public sealed record IntentDetectionRequest
|
||||
{
|
||||
/// <summary>Gets the query to analyze.</summary>
|
||||
public required string Query { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Response for intent detection.</summary>
|
||||
public sealed record IntentDetectionResponse
|
||||
{
|
||||
public required string Intent { get; init; }
|
||||
public required double Confidence { get; init; }
|
||||
public required string NormalizedInput { get; init; }
|
||||
public bool ExplicitSlashCommand { get; init; }
|
||||
public IntentParametersResponse? Parameters { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Intent parameters response.</summary>
|
||||
public sealed record IntentParametersResponse
|
||||
{
|
||||
public string? FindingId { get; init; }
|
||||
public string? Package { get; init; }
|
||||
public string? ImageReference { get; init; }
|
||||
public string? Environment { get; init; }
|
||||
public string? Duration { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Request for evidence preview.</summary>
|
||||
public sealed record EvidencePreviewRequest
|
||||
{
|
||||
public required string FindingId { get; init; }
|
||||
public string? ArtifactDigest { get; init; }
|
||||
public string? ImageReference { get; init; }
|
||||
public string? Environment { get; init; }
|
||||
public string? PackagePurl { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Response for evidence bundle preview.</summary>
|
||||
public sealed record EvidenceBundlePreviewResponse
|
||||
{
|
||||
public required string BundleId { get; init; }
|
||||
public string? FindingId { get; init; }
|
||||
public bool HasVexData { get; init; }
|
||||
public bool HasReachabilityData { get; init; }
|
||||
public bool HasBinaryPatchData { get; init; }
|
||||
public bool HasProvenanceData { get; init; }
|
||||
public bool HasPolicyData { get; init; }
|
||||
public bool HasOpsMemoryData { get; init; }
|
||||
public bool HasFixData { get; init; }
|
||||
public EvidenceSummary? EvidenceSummary { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Evidence summary.</summary>
|
||||
public sealed record EvidenceSummary
|
||||
{
|
||||
public string? VexStatus { get; init; }
|
||||
public string? ReachabilityStatus { get; init; }
|
||||
public bool? BinaryPatchDetected { get; init; }
|
||||
public string? PolicyDecision { get; init; }
|
||||
public int FixOptionsCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Chat service status response.</summary>
|
||||
public sealed record ChatServiceStatusResponse
|
||||
{
|
||||
public required bool Enabled { get; init; }
|
||||
public required string InferenceProvider { get; init; }
|
||||
public required string InferenceModel { get; init; }
|
||||
public required int MaxTokens { get; init; }
|
||||
public required bool GuardrailsEnabled { get; init; }
|
||||
public required bool AuditEnabled { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Error response.</summary>
|
||||
public sealed record ErrorResponse
|
||||
{
|
||||
public required string Error { get; init; }
|
||||
public string? Code { get; init; }
|
||||
public Dictionary<string, object>? Details { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
Reference in New Issue
Block a user