123 lines
4.8 KiB
C#
123 lines
4.8 KiB
C#
using IntelligenceApi.Engines;
|
|
using IntelligenceApi.Models;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using System.Text.Json;
|
|
|
|
namespace IntelligenceApi.Controllers;
|
|
|
|
/// <summary>
|
|
/// Internal execution endpoint — the single entry point for Gateway-routed requests.
|
|
///
|
|
/// Called exclusively by the Gateway's ExecutionService via:
|
|
/// POST /internal/execute
|
|
/// Headers: X-Internal-Key, X-Request-Id
|
|
/// Body: { "operation": "Ping" | "SpendDistribution", "payload": { ... } }
|
|
///
|
|
/// Operations:
|
|
/// Ping — liveness check, no payload required
|
|
/// SpendDistribution — routes to EngineRouter; payload maps to SpendDistributionRequest
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("internal/execute")]
|
|
public sealed class InternalController : ControllerBase
|
|
{
|
|
private readonly EngineRouter _router;
|
|
private readonly ILogger<InternalController> _log;
|
|
|
|
private static readonly JsonSerializerOptions _jsonOpts =
|
|
new(JsonSerializerDefaults.Web);
|
|
|
|
public InternalController(EngineRouter router, ILogger<InternalController> log)
|
|
{
|
|
_router = router;
|
|
_log = log;
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<IActionResult> Execute(
|
|
[FromBody] InternalExecuteRequest? request,
|
|
CancellationToken ct)
|
|
{
|
|
if (request == null)
|
|
return BadRequest(new { ok = false, error = "Request body required" });
|
|
|
|
var requestId = request.RequestId ?? HttpContext.Request.Headers["X-Request-Id"].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
|
|
var operation = (request.Operation ?? "").Trim();
|
|
|
|
_log.LogInformation(
|
|
"[Internal] Operation={Operation} RequestId={RequestId}",
|
|
operation, requestId);
|
|
|
|
return operation.ToLowerInvariant() switch
|
|
{
|
|
"ping" => Ok(new { ok = true, requestId, service = "IntelligenceApi", timestamp = DateTimeOffset.UtcNow }),
|
|
"spenddistribution" => await SpendDistribution(request.Payload, requestId, ct),
|
|
_ => BadRequest(new { ok = false, requestId, error = $"Unknown operation: '{operation}'" })
|
|
};
|
|
}
|
|
|
|
// ── Spend Distribution ───────────────────────────────────────────────────
|
|
|
|
private async Task<IActionResult> SpendDistribution(
|
|
JsonElement? payload,
|
|
string requestId,
|
|
CancellationToken ct)
|
|
{
|
|
if (payload == null || payload.Value.ValueKind == JsonValueKind.Null)
|
|
return BadRequest(new { ok = false, requestId, error = "Payload required for SpendDistribution" });
|
|
|
|
SpendDistributionRequest? distRequest;
|
|
try
|
|
{
|
|
distRequest = JsonSerializer.Deserialize<SpendDistributionRequest>(
|
|
payload.Value.GetRawText(), _jsonOpts);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogWarning("[Internal] Payload deserialize failed | RequestId={RequestId} Error={Error}",
|
|
requestId, ex.Message);
|
|
return BadRequest(new { ok = false, requestId, error = "Invalid payload shape for SpendDistribution" });
|
|
}
|
|
|
|
if (distRequest == null)
|
|
return BadRequest(new { ok = false, requestId, error = "Payload required for SpendDistribution" });
|
|
|
|
if (distRequest.MonthlyBudget <= 0)
|
|
return BadRequest(new { ok = false, requestId, error = "monthlyBudget must be greater than zero" });
|
|
|
|
if (distRequest.Keywords.Count == 0)
|
|
return BadRequest(new { ok = false, requestId, error = "At least one keyword is required" });
|
|
|
|
try
|
|
{
|
|
var engine = _router.Resolve(distRequest.ClientCategory);
|
|
var response = await engine.RecommendAsync(distRequest, ct);
|
|
|
|
_log.LogInformation(
|
|
"[Internal] SpendDistribution OK | RequestId={RequestId} Engine={Engine} Channels={N}",
|
|
requestId, response.Metadata.Engine, response.Channels.Count);
|
|
|
|
return Ok(response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_log.LogError(ex, "[Internal] SpendDistribution error | RequestId={RequestId}", requestId);
|
|
return StatusCode(500, new { ok = false, requestId, error = "Intelligence service error" });
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Request model ────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Shape sent by Gateway's ExecutionService to every provider container.
|
|
/// Matches the object built in ExecutionService.BuildProviderRequest().
|
|
/// </summary>
|
|
public sealed class InternalExecuteRequest
|
|
{
|
|
public string? Operation { get; set; }
|
|
public string? RequestId { get; set; }
|
|
public string? TenantId { get; set; }
|
|
public JsonElement? Payload { get; set; }
|
|
}
|