affiliate-launch-kit / backend /src /modules /generators /generators.service.ts
mohmmed5787's picture
Deploying Affiliate Launch Kit - Production Ready
d44ff09 verified
Raw
History Blame Contribute Delete
4.67 kB
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, EntityManager } from 'typeorm';
import { GeneratedAsset } from '../../database/entities';
import { AiEngineService } from './ai-engine/ai-engine.service';
import { mockGenerate } from './ai-engine/mock-generator';
import { STRATEGIES } from './strategies';
import { AssetType, GeneratorContext, ALL_ASSET_TYPES } from './generators.types';
export interface GenerateOutcome {
asset_type: AssetType;
ok: boolean;
version: number;
error?: string;
}
/**
* Orchestrates the common processing pattern (Section 3.0) for all 5 generators:
* 1. apply defaults 2. run AI engine 3. validate schema
* 4. single retry w/ stricter instruction 5. finalize (sanitize/normalize)
* 6. persist (version increments on regenerate).
*
* `manager` may be passed to execute persistence inside an outer transaction
* (audit fix #4: a Campaign and all its GeneratedAssets commit atomically).
*/
@Injectable()
export class GeneratorsService {
private readonly logger = new Logger('Generators');
constructor(
@InjectRepository(GeneratedAsset) private readonly assetRepo: Repository<GeneratedAsset>,
private readonly aiEngine: AiEngineService,
) {}
/** Generate a single asset for a campaign (used by initial run + regenerate). */
async generateAsset(
campaignId: string,
assetType: AssetType,
ctx: GeneratorContext,
customInstruction?: string,
manager?: EntityManager,
): Promise<GeneratedAsset> {
const strategy = STRATEGIES[assetType];
if (!strategy) throw new BadRequestException({ code: 'UNKNOWN_ASSET_TYPE', message: `Unknown asset type ${assetType}` });
const enrichedCtx = strategy.applyDefaults(ctx);
const outcome = await this.runWithValidation(assetType, enrichedCtx, customInstruction, strategy);
return this.persistAsset(campaignId, assetType, outcome.content, strategy.format, manager);
}
/** Run all selected generators for a campaign (used by POST /campaigns). */
async generateForCampaign(
campaignId: string,
selected: AssetType[],
ctx: GeneratorContext,
manager?: EntityManager,
): Promise<GenerateOutcome[]> {
const types = selected && selected.length ? selected : [...ALL_ASSET_TYPES];
const outcomes: GenerateOutcome[] = [];
for (const assetType of types) {
try {
const asset = await this.generateAsset(campaignId, assetType, ctx, undefined, manager);
outcomes.push({ asset_type: assetType, ok: true, version: asset.version });
} catch (err) {
this.logger.error(`Generation failed for ${assetType}: ${(err as Error).message}`);
outcomes.push({ asset_type: assetType, ok: false, version: 0, error: (err as Error).message });
}
}
return outcomes;
}
private async runWithValidation(
assetType: AssetType,
ctx: GeneratorContext,
customInstruction: string | undefined,
strategy: (typeof STRATEGIES)[AssetType],
): Promise<{ content: string }> {
const { content: raw1 } = await this.aiEngine.runGeneration(assetType, ctx, customInstruction);
if (strategy.validate(raw1, ctx)) {
return { content: strategy.finalize(raw1) };
}
this.logger.warn(`First-pass validation failed for ${assetType}; retrying once with stricter instruction.`);
const stricter = (customInstruction ? customInstruction + ' ' : '') + strategy.retryInstruction(ctx);
const { content: raw2 } = await this.aiEngine.runGeneration(assetType, ctx, stricter);
if (strategy.validate(raw2, ctx)) {
return { content: strategy.finalize(raw2) };
}
this.logger.warn(`Retry still invalid for ${assetType}; using deterministic fallback.`);
const fallback = mockGenerate(assetType, ctx);
return { content: strategy.finalize(fallback) };
}
private async persistAsset(
campaignId: string,
assetType: AssetType,
content: string,
format: string,
manager?: EntityManager,
): Promise<GeneratedAsset> {
const repo = manager ? manager.getRepository(GeneratedAsset) : this.assetRepo;
const existing = await repo.findOne({ where: { campaign_id: campaignId, asset_type: assetType } });
if (existing) {
existing.content = content;
existing.version = existing.version + 1;
existing.is_manual_edit = false;
return repo.save(existing);
}
const asset = repo.create({
campaign_id: campaignId,
asset_type: assetType,
content_format: format,
content,
version: 1,
is_manual_edit: false,
});
return repo.save(asset);
}
}