Initial import into Gitea
This commit is contained in:
48
IntelligenceApi/Controllers/DemographicsController.cs
Normal file
48
IntelligenceApi/Controllers/DemographicsController.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using IntelligenceApi.Engines;
|
||||
using IntelligenceApi.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace IntelligenceApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Demographics analysis endpoint.
|
||||
///
|
||||
/// Called exclusively by the Gateway's IntelligenceApiClient after it fetches
|
||||
/// raw census data from the database. This container derives audience
|
||||
/// recommendations from the raw data — age chips, income tiers, insights.
|
||||
///
|
||||
/// POST /api/demographics/analyze
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/demographics")]
|
||||
public sealed class DemographicsController : ControllerBase
|
||||
{
|
||||
private readonly DemographicsAnalyzer _analyzer;
|
||||
private readonly ILogger<DemographicsController> _log;
|
||||
|
||||
public DemographicsController(DemographicsAnalyzer analyzer, ILogger<DemographicsController> log)
|
||||
{
|
||||
_analyzer = analyzer;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
[HttpPost("analyze")]
|
||||
public IActionResult Analyze([FromBody] DemographicAnalysisRequest? request)
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Zcta))
|
||||
return BadRequest(new { ok = false, error = "zcta and census data are required" });
|
||||
|
||||
_log.LogInformation("[Demographics] Analyze | ZCTA={Zcta}", request.Zcta);
|
||||
|
||||
try
|
||||
{
|
||||
var result = _analyzer.Analyze(request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "[Demographics] Analysis error | ZCTA={Zcta}", request.Zcta);
|
||||
return StatusCode(500, new { ok = false, error = "Demographics analysis error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
122
IntelligenceApi/Controllers/InternalController.cs
Normal file
122
IntelligenceApi/Controllers/InternalController.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
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; }
|
||||
}
|
||||
67
IntelligenceApi/Controllers/SpendDistributionController.cs
Normal file
67
IntelligenceApi/Controllers/SpendDistributionController.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using IntelligenceApi.Engines;
|
||||
using IntelligenceApi.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace IntelligenceApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Spend distribution endpoint — the single public surface of IntelligenceApi.
|
||||
///
|
||||
/// Called exclusively by the Gateway (never directly by the client portal).
|
||||
/// The Gateway injects clientCategory from ClientContext before forwarding.
|
||||
///
|
||||
/// POST /api/spend-distribution
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/spend-distribution")]
|
||||
public sealed class SpendDistributionController : ControllerBase
|
||||
{
|
||||
private readonly EngineRouter _router;
|
||||
private readonly ILogger<SpendDistributionController> _log;
|
||||
|
||||
public SpendDistributionController(EngineRouter router, ILogger<SpendDistributionController> log)
|
||||
{
|
||||
_router = router;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a spend distribution recommendation.
|
||||
/// clientCategory in the request body determines which engine runs.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Recommend(
|
||||
[FromBody] SpendDistributionRequest? request,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (request == null)
|
||||
return BadRequest(new { ok = false, error = "Request body required" });
|
||||
|
||||
if (request.MonthlyBudget <= 0)
|
||||
return BadRequest(new { ok = false, error = "monthlyBudget must be greater than zero" });
|
||||
|
||||
if (request.Keywords.Count == 0)
|
||||
return BadRequest(new { ok = false, error = "At least one keyword is required" });
|
||||
|
||||
_log.LogInformation(
|
||||
"[SpendDistribution] Request | Category={Category} Budget={Budget} Objective={Obj}",
|
||||
request.ClientCategory, request.MonthlyBudget, request.Objective);
|
||||
|
||||
try
|
||||
{
|
||||
var engine = _router.Resolve(request.ClientCategory);
|
||||
var response = await engine.RecommendAsync(request, ct);
|
||||
|
||||
_log.LogInformation(
|
||||
"[SpendDistribution] OK | Engine={Engine} Channels={N}",
|
||||
response.Metadata.Engine, response.Channels.Count);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError(ex, "[SpendDistribution] Unhandled error");
|
||||
return StatusCode(500, new { ok = false, error = "Intelligence service error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user