Initial import into Gitea

This commit is contained in:
Grae Jones
2026-03-14 13:50:09 -07:00
parent 8e7e03702e
commit 34c1f09e01
154 changed files with 17666 additions and 1548 deletions

View 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" });
}
}
}

View 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; }
}

View 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" });
}
}
}