Files
AdPlatform-Server/Creative/Services/CreativeService.cs
2026-03-14 13:50:09 -07:00

231 lines
8.2 KiB
C#

using System.Text.Json;
using Creative.Models;
namespace Creative.Services;
/// <summary>
/// Main creative service - dispatches operations to appropriate handlers.
/// Stateless: returns JSON, Gateway handles persistence.
/// </summary>
public class CreativeService
{
private readonly ScraperService _scraper;
private readonly CopyGeneratorService _copyGen;
private readonly ImageGeneratorService _imageGen;
private readonly ILogger<CreativeService> _logger;
public CreativeService(
ScraperService scraper,
CopyGeneratorService copyGen,
ImageGeneratorService imageGen,
ILogger<CreativeService> logger)
{
_scraper = scraper;
_copyGen = copyGen;
_imageGen = imageGen;
_logger = logger;
}
/// <summary>
/// Main dispatch method - routes to appropriate operation handler.
/// </summary>
public async Task<CreativeResponse> ExecuteAsync(CreativeRequest request, CancellationToken ct)
{
var requestId = request.RequestId ?? Guid.NewGuid().ToString("N");
var operation = (request.Operation ?? "").Trim();
_logger.LogInformation("[Creative] Executing {Operation} | RequestId={RequestId}",
operation, requestId);
try
{
return operation switch
{
"Ping" => Ping(requestId),
"AnalyzeUrl" => await AnalyzeUrlAsync(request, requestId, ct),
"GenerateAssets" => await GenerateAssetsAsync(request, requestId, ct),
"GetImages" => await GetImagesAsync(request, requestId, ct),
"CreateDraft" => await CreateDraftAsync(request, requestId, ct),
_ => CreativeResponse.Fail(requestId, "UNKNOWN_OPERATION",
$"Unknown operation: {operation}")
};
}
catch (Exception ex)
{
_logger.LogError(ex, "[Creative] {Operation} failed | RequestId={RequestId}",
operation, requestId);
return CreativeResponse.Fail(requestId, "INTERNAL_ERROR", ex.Message);
}
}
// ============================================================
// Operations
// ============================================================
/// <summary>
/// Health check.
/// </summary>
private static CreativeResponse Ping(string requestId)
{
return CreativeResponse.Success(requestId, new
{
pong = true,
timestamp = DateTimeOffset.UtcNow
});
}
/// <summary>
/// Scrape and analyze a URL. Returns structured content.
/// Payload: { "url": "https://..." }
/// </summary>
private async Task<CreativeResponse> AnalyzeUrlAsync(
CreativeRequest request, string requestId, CancellationToken ct)
{
var url = GetPayloadString(request, "url");
if (string.IsNullOrWhiteSpace(url))
return CreativeResponse.Fail(requestId, "MISSING_URL", "payload.url is required");
url = NormalizeUrl(url);
var analysis = await _scraper.AnalyzeUrlAsync(url, ct);
return CreativeResponse.Success(requestId, analysis);
}
/// <summary>
/// Generate text assets from previously analyzed content.
/// Payload: { "analysis": { ... } } (UrlAnalysis object)
/// </summary>
private async Task<CreativeResponse> GenerateAssetsAsync(
CreativeRequest request, string requestId, CancellationToken ct)
{
var analysisJson = GetPayloadObject(request, "analysis");
if (analysisJson == null)
return CreativeResponse.Fail(requestId, "MISSING_ANALYSIS",
"payload.analysis is required (pass result from AnalyzeUrl)");
var analysis = JsonSerializer.Deserialize<UrlAnalysis>(analysisJson.Value.GetRawText());
if (analysis == null)
return CreativeResponse.Fail(requestId, "INVALID_ANALYSIS",
"Could not deserialize analysis object");
var (headlines, descriptions, source) = await _copyGen.GenerateAsync(analysis, ct);
return CreativeResponse.Success(requestId, new
{
headlines,
descriptions,
source
});
}
/// <summary>
/// Get images matching previously analyzed content.
/// Payload: { "analysis": { ... } } (UrlAnalysis object)
/// </summary>
private async Task<CreativeResponse> GetImagesAsync(
CreativeRequest request, string requestId, CancellationToken ct)
{
var analysisJson = GetPayloadObject(request, "analysis");
if (analysisJson == null)
return CreativeResponse.Fail(requestId, "MISSING_ANALYSIS",
"payload.analysis is required (pass result from AnalyzeUrl)");
var analysis = JsonSerializer.Deserialize<UrlAnalysis>(analysisJson.Value.GetRawText());
if (analysis == null)
return CreativeResponse.Fail(requestId, "INVALID_ANALYSIS",
"Could not deserialize analysis object");
var (images, source) = await _imageGen.GenerateAsync(analysis, ct);
return CreativeResponse.Success(requestId, new
{
images,
source
});
}
/// <summary>
/// Full pipeline: scrape URL → generate copy → source images → return campaign draft.
/// Payload: { "url": "https://..." }
/// Gateway persists the returned draft to tbCreativeDraft.
/// </summary>
private async Task<CreativeResponse> CreateDraftAsync(
CreativeRequest request, string requestId, CancellationToken ct)
{
var url = GetPayloadString(request, "url");
if (string.IsNullOrWhiteSpace(url))
return CreativeResponse.Fail(requestId, "MISSING_URL", "payload.url is required");
url = NormalizeUrl(url);
// Step 1: Scrape
_logger.LogInformation("[Creative] CreateDraft step 1/3: scraping {Url}", url);
var analysis = await _scraper.AnalyzeUrlAsync(url, ct);
// Step 2: Generate copy
_logger.LogInformation("[Creative] CreateDraft step 2/3: generating copy");
var (headlines, descriptions, copySource) = await _copyGen.GenerateAsync(analysis, ct);
// Step 3: Source images
_logger.LogInformation("[Creative] CreateDraft step 3/3: sourcing images");
var (images, imageSource) = await _imageGen.GenerateAsync(analysis, ct);
// Assemble draft - Gateway will persist this
var draftId = Guid.NewGuid().ToString("N")[..12];
var draft = new CampaignDraft
{
DraftId = draftId,
Url = url,
Analysis = analysis,
Headlines = headlines,
Descriptions = descriptions,
Images = images,
Source = copySource,
ImageSource = imageSource,
CreatedAt = DateTimeOffset.UtcNow
};
_logger.LogInformation(
"[Creative] Draft assembled | DraftId={DraftId} Headlines={H} Descriptions={D} Images={I} CopySource={CS} ImageSource={IS}",
draftId, headlines.Count, descriptions.Count, images.Count, copySource, imageSource);
return CreativeResponse.Success(requestId, draft);
}
// ============================================================
// Helpers
// ============================================================
private static string? GetPayloadString(CreativeRequest request, string key)
{
if (request.Payload == null) return null;
if (!request.Payload.TryGetValue(key, out var value)) return null;
return value switch
{
string s => s,
JsonElement je when je.ValueKind == JsonValueKind.String => je.GetString(),
_ => value?.ToString()
};
}
private static JsonElement? GetPayloadObject(CreativeRequest request, string key)
{
if (request.Payload == null) return null;
if (!request.Payload.TryGetValue(key, out var value)) return null;
if (value is JsonElement je && je.ValueKind == JsonValueKind.Object)
return je;
return null;
}
private static string NormalizeUrl(string url)
{
url = url.Trim();
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
url = "https://" + url;
return url;
}
}