231 lines
8.2 KiB
C#
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;
|
|
}
|
|
}
|