Initial import into Gitea
This commit is contained in:
214
Gateway/Services/IntelligenceApiClient.cs
Normal file
214
Gateway/Services/IntelligenceApiClient.cs
Normal file
@@ -0,0 +1,214 @@
|
||||
using Gateway.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Gateway.Services;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for IntelligenceApi — the spend distribution engine container.
|
||||
///
|
||||
/// The Gateway injects clientCategory from ClientContext and provider config
|
||||
/// before forwarding requests. The client portal never calls IntelligenceApi
|
||||
/// directly; all routing goes through the Gateway.
|
||||
///
|
||||
/// FALLBACK: If IntelligenceApi is unreachable, ForecastController falls back
|
||||
/// to the local ForecastService (identical to the General engine output).
|
||||
/// This means a container restart or deployment never breaks the wizard.
|
||||
/// </summary>
|
||||
public sealed class IntelligenceApiClient
|
||||
{
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly IConfiguration _cfg;
|
||||
private readonly ILogger<IntelligenceApiClient> _logger;
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOpts =
|
||||
new(JsonSerializerDefaults.Web);
|
||||
|
||||
public IntelligenceApiClient(
|
||||
IHttpClientFactory http,
|
||||
IConfiguration cfg,
|
||||
ILogger<IntelligenceApiClient> logger)
|
||||
{
|
||||
_http = http;
|
||||
_cfg = cfg;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forward raw census data for a ZCTA to the Intelligence container for
|
||||
/// market analysis derivation (age chips, income tiers, insight strings).
|
||||
/// Returns the raw JSON response string, or null if the container is
|
||||
/// unreachable — caller falls back to returning raw census data.
|
||||
/// </summary>
|
||||
public async Task<string?> GetDemographicAnalysisAsync(
|
||||
string zcta,
|
||||
JsonElement censusData,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var baseUrl = _cfg["INTELLIGENCE_API_URL"]
|
||||
?? Environment.GetEnvironmentVariable("INTELLIGENCE_API_URL");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
_logger.LogDebug("[IntelligenceApiClient] INTELLIGENCE_API_URL not configured — skipping demographics analysis");
|
||||
return null;
|
||||
}
|
||||
|
||||
var internalKey = _cfg["INTELLIGENCE_INTERNAL_KEY"]
|
||||
?? Environment.GetEnvironmentVariable("INTELLIGENCE_INTERNAL_KEY");
|
||||
|
||||
var payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
zcta,
|
||||
census = censusData
|
||||
}, _jsonOpts);
|
||||
|
||||
try
|
||||
{
|
||||
var client = _http.CreateClient();
|
||||
using var msg = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
$"{baseUrl.TrimEnd('/')}/api/demographics/analyze");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(internalKey))
|
||||
msg.Headers.Add("X-Internal-Key", internalKey);
|
||||
|
||||
msg.Content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");
|
||||
|
||||
_logger.LogInformation("[IntelligenceApiClient] Demographics analysis | ZCTA={Zcta}", zcta);
|
||||
|
||||
using var resp = await client.SendAsync(msg, ct);
|
||||
var body = await resp.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("[IntelligenceApiClient] Demographics non-success {Status}", (int)resp.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_logger.LogWarning("[IntelligenceApiClient] Demographics analysis timed out | ZCTA={Zcta}", zcta);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[IntelligenceApiClient] Demographics analysis failed | ZCTA={Zcta}", zcta);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forward a channel forecast request to IntelligenceApi with
|
||||
/// clientCategory injected. Returns null if the service is unreachable
|
||||
/// or returns an error — caller should fall back to ForecastService.
|
||||
/// </summary>
|
||||
public async Task<ChannelForecastResponse?> GetSpendDistributionAsync(
|
||||
ChannelForecastRequest request,
|
||||
string? clientCategory,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var baseUrl = _cfg["INTELLIGENCE_API_URL"]
|
||||
?? Environment.GetEnvironmentVariable("INTELLIGENCE_API_URL");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
_logger.LogDebug("[IntelligenceApiClient] INTELLIGENCE_API_URL not configured — skipping");
|
||||
return null;
|
||||
}
|
||||
|
||||
var internalKey = _cfg["INTELLIGENCE_INTERNAL_KEY"]
|
||||
?? Environment.GetEnvironmentVariable("INTELLIGENCE_INTERNAL_KEY");
|
||||
|
||||
// Build the IntelligenceApi request — forward everything from the
|
||||
// original wizard request, plus inject clientCategory and provider config
|
||||
var intelligenceRequest = new
|
||||
{
|
||||
clientCategory = clientCategory ?? "General",
|
||||
objective = request.Objective,
|
||||
businessCategory = request.BusinessCategory,
|
||||
keywords = request.Keywords,
|
||||
geoTargeting = request.GeoTargeting,
|
||||
audience = request.Audience,
|
||||
monthlyBudget = request.MonthlyBudget,
|
||||
channels = request.Channels,
|
||||
|
||||
// Forward provider URLs so the engine can call providers directly
|
||||
providerUrls = new Dictionary<string, string>
|
||||
{
|
||||
["google_ads"] = _cfg["GOOGLE_PROVIDER_URL"]
|
||||
?? Environment.GetEnvironmentVariable("GOOGLE_PROVIDER_URL") ?? ""
|
||||
},
|
||||
internalKeys = new Dictionary<string, string>
|
||||
{
|
||||
["google_ads"] = _cfg["GOOGLE_INTERNAL_KEY"]
|
||||
?? Environment.GetEnvironmentVariable("GOOGLE_INTERNAL_KEY") ?? ""
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var client = _http.CreateClient();
|
||||
using var msg = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
$"{baseUrl.TrimEnd('/')}/api/spend-distribution");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(internalKey))
|
||||
msg.Headers.Add("X-Internal-Key", internalKey);
|
||||
|
||||
msg.Content = new StringContent(
|
||||
JsonSerializer.Serialize(intelligenceRequest, _jsonOpts),
|
||||
System.Text.Encoding.UTF8, "application/json");
|
||||
|
||||
_logger.LogInformation(
|
||||
"[IntelligenceApiClient] Calling engine | Category={Category} Budget={Budget}",
|
||||
clientCategory, request.MonthlyBudget);
|
||||
|
||||
using var resp = await client.SendAsync(msg, ct);
|
||||
var body = await resp.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"[IntelligenceApiClient] Non-success {Status}: {Body}",
|
||||
(int)resp.StatusCode, body[..Math.Min(body.Length, 200)]);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Map IntelligenceApi response shape → Gateway ChannelForecastResponse
|
||||
// The shapes are intentionally aligned so this is a straight deserialize.
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("ok", out var okProp) || !okProp.GetBoolean())
|
||||
{
|
||||
_logger.LogWarning("[IntelligenceApiClient] Engine returned ok=false");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Re-serialize then deserialize into the Gateway model
|
||||
// (avoids a hard dependency on IntelligenceApi model types in Gateway)
|
||||
var result = JsonSerializer.Deserialize<ChannelForecastResponse>(body, _jsonOpts);
|
||||
|
||||
_logger.LogInformation(
|
||||
"[IntelligenceApiClient] OK | Engine={Engine} Channels={N}",
|
||||
root.TryGetProperty("metadata", out var meta)
|
||||
&& meta.TryGetProperty("engine", out var eng)
|
||||
? eng.GetString() : "?",
|
||||
result?.Channels?.Count ?? 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
_logger.LogWarning("[IntelligenceApiClient] Request timed out");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "[IntelligenceApiClient] Request failed");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user