refactor(ai): introduce GeneratorFormat enum and drop stringly-typed format
Replace the 'single'/'carousel' string format threaded through the AI generation path with a typed GeneratorFormat enum (PostContentGenerator, PostContentHumanizer, PostContentStreamer, StreamPostCreation, RunGenerateNode, deriveFormat). Remove generatorFormat() from AiContentTemplate and the three templates — it conflated style and format (returning 'tweet_card'). The tweet-card check now lives on ContentStyle via isTweetCard(), and the humanizer skip on ContentStyle::humanizes(), so the style declares its own behaviour instead of callers comparing magic strings.
This commit is contained in:
parent
773d2c729f
commit
9f5bacc03f
18 changed files with 104 additions and 75 deletions
|
|
@ -11,6 +11,7 @@
|
|||
use App\Ai\Templates\TemplateContext;
|
||||
use App\DataTransferObjects\Automation\NodeRunResult;
|
||||
use App\Enums\Ai\ContentStyle;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Models\AutomationRun;
|
||||
use App\Models\SocialAccount;
|
||||
|
|
@ -84,13 +85,13 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
|||
? $activeAccounts->get(data_get($platforms[0], 'social_account_id'))
|
||||
: null;
|
||||
|
||||
$isCarousel = $format === 'carousel';
|
||||
$isCarousel = $format->isCarousel();
|
||||
$imageCount = $isCarousel ? $slideCount : ($wantsImage ? 1 : 0);
|
||||
|
||||
$templateContext = new TemplateContext(
|
||||
workspace: $workspace,
|
||||
socialAccount: $brandAccount,
|
||||
format: $platformContext ?? $format,
|
||||
format: $platformContext ?? $format->value,
|
||||
imageCount: $imageCount,
|
||||
isCarousel: $isCarousel,
|
||||
applyBrandVisuals: (bool) data_get($config, 'use_brand_visuals', true),
|
||||
|
|
@ -114,12 +115,12 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
|||
completionTokens: $generatorResponse->usage->completionTokens,
|
||||
provider: (string) config('ai.default'),
|
||||
model: (string) config('ai.default_text_model'),
|
||||
metadata: ['agent' => 'post_generator', 'format' => $format, 'source' => 'automation'],
|
||||
metadata: ['agent' => 'post_generator', 'format' => $format->value, 'source' => 'automation'],
|
||||
);
|
||||
|
||||
$structured = $generatorResponse->structured ?? [];
|
||||
|
||||
$structured = $this->humanize($workspace, $structured, $format, $style->value, $applyBrandVoice, $platformContext);
|
||||
$structured = $this->humanize($workspace, $structured, $format, $style, $applyBrandVoice, $platformContext);
|
||||
|
||||
$intendedImageCount = $this->intendedImageCount($format, $slideCount, $wantsImage, $structured, $brandAccount, $style);
|
||||
|
||||
|
|
@ -161,14 +162,14 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
|||
* @param array<string, mixed> $structured
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function humanize(Workspace $workspace, array $structured, string $format, string $styleKey = 'image_card', bool $applyBrandVoice = true, ?string $platformContext = null): array
|
||||
private function humanize(Workspace $workspace, array $structured, GeneratorFormat $format, ContentStyle $style, bool $applyBrandVoice = true, ?string $platformContext = null): array
|
||||
{
|
||||
if ($styleKey === 'tweet_card' || $styleKey === 'tweet_card_image') {
|
||||
if (! $style->humanizes()) {
|
||||
return $structured;
|
||||
}
|
||||
|
||||
try {
|
||||
$input = $format === 'carousel'
|
||||
$input = $format->isCarousel()
|
||||
? [
|
||||
'caption' => data_get($structured, 'caption', ''),
|
||||
'slides' => array_map(
|
||||
|
|
@ -195,10 +196,10 @@ private function humanize(Workspace $workspace, array $structured, string $forma
|
|||
completionTokens: $response->usage->completionTokens,
|
||||
provider: (string) config('ai.default'),
|
||||
model: (string) config('ai.default_text_model'),
|
||||
metadata: ['agent' => 'post_humanizer', 'format' => $format, 'source' => 'automation'],
|
||||
metadata: ['agent' => 'post_humanizer', 'format' => $format->value, 'source' => 'automation'],
|
||||
);
|
||||
|
||||
if ($format === 'carousel') {
|
||||
if ($format->isCarousel()) {
|
||||
$structured['caption'] = data_get($humanized, 'caption', $structured['caption'] ?? '');
|
||||
$originalSlides = $structured['slides'] ?? [];
|
||||
$humanizedSlides = data_get($humanized, 'slides', []);
|
||||
|
|
@ -232,15 +233,15 @@ private function humanize(Workspace $workspace, array $structured, string $forma
|
|||
*
|
||||
* @param array<string, mixed> $structured
|
||||
*/
|
||||
private function extractContent(array $structured, string $format, ContentStyle $style): string
|
||||
private function extractContent(array $structured, GeneratorFormat $format, ContentStyle $style): string
|
||||
{
|
||||
if ($style === ContentStyle::TweetCard || $style === ContentStyle::TweetCardImage) {
|
||||
return $format === 'carousel'
|
||||
if ($style->isTweetCard()) {
|
||||
return $format->isCarousel()
|
||||
? (string) data_get($structured, 'caption', '')
|
||||
: (string) data_get($structured, 'tweet_text', '');
|
||||
}
|
||||
|
||||
return $format === 'carousel'
|
||||
return $format->isCarousel()
|
||||
? (string) data_get($structured, 'caption', '')
|
||||
: (string) data_get($structured, 'content', '');
|
||||
}
|
||||
|
|
@ -261,7 +262,7 @@ private function extractContent(array $structured, string $format, ContentStyle
|
|||
*
|
||||
* @param array<int, array{social_account_id: string, content_type: ?string, meta: array<string, mixed>}> $accountsConfig
|
||||
* @param array<string, mixed> $config
|
||||
* @return array{format: string, slide_count: int}
|
||||
* @return array{format: GeneratorFormat, slide_count: int}
|
||||
*/
|
||||
public function deriveFormat(array $accountsConfig, array $config): array
|
||||
{
|
||||
|
|
@ -278,10 +279,10 @@ public function deriveFormat(array $accountsConfig, array $config): array
|
|||
if ($maxImagesAcross > 1 && $targetSlideCount > 1) {
|
||||
$cap = min(GenerateNodeValidator::MAX_GENERATED_IMAGES, $maxImagesAcross);
|
||||
|
||||
return ['format' => 'carousel', 'slide_count' => min($targetSlideCount, $cap)];
|
||||
return ['format' => GeneratorFormat::Carousel, 'slide_count' => min($targetSlideCount, $cap)];
|
||||
}
|
||||
|
||||
return ['format' => 'single', 'slide_count' => 1];
|
||||
return ['format' => GeneratorFormat::Single, 'slide_count' => 1];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -311,17 +312,17 @@ private function resolvePlatformContext(array $accountsConfig): ?string
|
|||
*
|
||||
* @param array<string, mixed> $structured
|
||||
*/
|
||||
private function intendedImageCount(string $format, int $slideCount, bool $wantsImage, array $structured, ?SocialAccount $brandAccount, ContentStyle $style): int
|
||||
private function intendedImageCount(GeneratorFormat $format, int $slideCount, bool $wantsImage, array $structured, ?SocialAccount $brandAccount, ContentStyle $style): int
|
||||
{
|
||||
if (! $brandAccount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($style === ContentStyle::TweetCard || $style === ContentStyle::TweetCardImage) {
|
||||
return $format === 'carousel' ? $slideCount : 1;
|
||||
if ($style->isTweetCard()) {
|
||||
return $format->isCarousel() ? $slideCount : 1;
|
||||
}
|
||||
|
||||
if ($format === 'carousel') {
|
||||
if ($format->isCarousel()) {
|
||||
$slides = data_get($structured, 'slides', []);
|
||||
|
||||
return is_array($slides) ? count($slides) : $slideCount;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Ai\Agents\Concerns\ResolvesPlatformCopyBudget;
|
||||
use App\Ai\Templates\AiContentTemplate;
|
||||
use App\Ai\Templates\TemplateContext;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Ai\TemplateContextResolver;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
|
|
@ -25,7 +26,7 @@ class PostContentGenerator implements Agent, HasStructuredOutput
|
|||
public function __construct(
|
||||
public Workspace $workspace,
|
||||
public ?string $currentContent = null,
|
||||
public string $format = 'single',
|
||||
public GeneratorFormat $format = GeneratorFormat::Single,
|
||||
public int $slideCount = 1,
|
||||
public ?string $platformContext = null,
|
||||
public bool $applyBrandVoice = true,
|
||||
|
|
@ -51,7 +52,7 @@ public function instructions(): string
|
|||
|
||||
$budget = $this->platformCopyBudget($this->platformContext);
|
||||
|
||||
if ($this->template?->generatorFormat() === 'tweet_card') {
|
||||
if ($this->template?->style()->isTweetCard()) {
|
||||
$budget['hard_max_chars'] = 560;
|
||||
$budget['target_chars'] = 280;
|
||||
}
|
||||
|
|
@ -66,7 +67,7 @@ public function instructions(): string
|
|||
'brand_voice_traits' => $this->applyBrandVoice ? ($this->workspace->brand_voice_traits ?? []) : [],
|
||||
'content_language' => $this->workspace->content_language,
|
||||
'current_content' => $this->currentContent,
|
||||
'format' => $this->format,
|
||||
'format' => $this->format->value,
|
||||
'slide_count' => $this->slideCount,
|
||||
'examples' => $examples,
|
||||
'hard_max_chars' => $budget['hard_max_chars'],
|
||||
|
|
@ -81,7 +82,7 @@ public function schema(JsonSchema $schema): array
|
|||
return $this->template->schema($schema, $this->templateContext);
|
||||
}
|
||||
|
||||
if ($this->format === 'carousel') {
|
||||
if ($this->format->isCarousel()) {
|
||||
return [
|
||||
'caption' => $schema->string()->description('The Instagram caption for the carousel post.')->required(),
|
||||
'slides' => $schema->array()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
namespace App\Ai\Agents;
|
||||
|
||||
use App\Ai\Agents\Concerns\ResolvesPlatformCopyBudget;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Ai\Attributes\Temperature;
|
||||
|
|
@ -27,7 +28,7 @@ class PostContentHumanizer implements Agent, HasStructuredOutput
|
|||
|
||||
public function __construct(
|
||||
public Workspace $workspace,
|
||||
public string $format = 'single',
|
||||
public GeneratorFormat $format = GeneratorFormat::Single,
|
||||
public ?string $platformContext = null,
|
||||
public bool $applyBrandVoice = true,
|
||||
) {}
|
||||
|
|
@ -43,7 +44,7 @@ public function instructions(): string
|
|||
'brand_name' => $this->workspace->name ?? '',
|
||||
'brand_voice_traits' => $this->applyBrandVoice ? ($this->workspace->brand_voice_traits ?? []) : [],
|
||||
'content_language' => $this->workspace->content_language,
|
||||
'format' => $this->format,
|
||||
'format' => $this->format->value,
|
||||
'hard_max_chars' => $budget['hard_max_chars'],
|
||||
'target_chars' => $budget['target_chars'],
|
||||
'platform_label' => $budget['platform_label'],
|
||||
|
|
@ -52,7 +53,7 @@ public function instructions(): string
|
|||
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
if ($this->format === 'carousel') {
|
||||
if ($this->format->isCarousel()) {
|
||||
return [
|
||||
'caption' => $schema->string()->description('The humanized Instagram caption.')->required(),
|
||||
'slides' => $schema->array()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Models\Workspace;
|
||||
use Laravel\Ai\Attributes\Temperature;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
|
|
@ -36,7 +37,7 @@ public function instructions(): string
|
|||
'brand_voice_traits' => $this->workspace->brand_voice_traits ?? [],
|
||||
'content_language' => $this->workspace->content_language,
|
||||
'current_content' => $this->currentContent,
|
||||
'format' => 'single',
|
||||
'format' => GeneratorFormat::Single->value,
|
||||
'slide_count' => 1,
|
||||
])->render();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ public function needsAccount(): bool;
|
|||
*/
|
||||
public function supportedFormats(): array;
|
||||
|
||||
/** The 'format' string passed to the generator agent (e.g. 'single'). */
|
||||
public function generatorFormat(): string;
|
||||
|
||||
/** The Blade view path for the generator prompt. */
|
||||
public function promptView(TemplateContext $context): string;
|
||||
|
||||
|
|
|
|||
|
|
@ -49,11 +49,6 @@ public function supportedFormats(): array
|
|||
return [];
|
||||
}
|
||||
|
||||
public function generatorFormat(): string
|
||||
{
|
||||
return 'single';
|
||||
}
|
||||
|
||||
public function promptView(TemplateContext $context): string
|
||||
{
|
||||
return 'prompts.post_content.generator';
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ public function supportedFormats(): array
|
|||
return [];
|
||||
}
|
||||
|
||||
public function generatorFormat(): string
|
||||
{
|
||||
return 'tweet_card';
|
||||
}
|
||||
|
||||
public function promptView(TemplateContext $context): string
|
||||
{
|
||||
return $context->isCarousel
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ public function supportedFormats(): array
|
|||
return [];
|
||||
}
|
||||
|
||||
public function generatorFormat(): string
|
||||
{
|
||||
return 'tweet_card';
|
||||
}
|
||||
|
||||
public function promptView(TemplateContext $context): string
|
||||
{
|
||||
return $context->isCarousel
|
||||
|
|
|
|||
|
|
@ -31,6 +31,24 @@ public function needsAccount(): bool
|
|||
};
|
||||
}
|
||||
|
||||
/** Whether this style's generated text is run through the humanizer pass. */
|
||||
public function humanizes(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::ImageCard => true,
|
||||
self::TweetCard, self::TweetCardImage => false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether this style renders the post as a tweet-style card. */
|
||||
public function isTweetCard(): bool
|
||||
{
|
||||
return match ($this) {
|
||||
self::TweetCard, self::TweetCardImage => true,
|
||||
self::ImageCard => false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Public path to the picker preview thumbnail. */
|
||||
public function previewAsset(): string
|
||||
{
|
||||
|
|
|
|||
16
app/Enums/Ai/GeneratorFormat.php
Normal file
16
app/Enums/Ai/GeneratorFormat.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Ai;
|
||||
|
||||
enum GeneratorFormat: string
|
||||
{
|
||||
case Single = 'single';
|
||||
case Carousel = 'carousel';
|
||||
|
||||
public function isCarousel(): bool
|
||||
{
|
||||
return $this === self::Carousel;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
use App\Ai\Templates\AiTemplateRegistry;
|
||||
use App\Ai\Templates\GeneratedPost;
|
||||
use App\Ai\Templates\TemplateContext;
|
||||
use App\Enums\Ai\ContentStyle;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Enums\Notification\Channel as NotificationChannel;
|
||||
use App\Enums\Notification\Type as NotificationType;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
|
|
@ -53,7 +55,7 @@ public function handle(): void
|
|||
$style = app(AiTemplateRegistry::class)->find($this->template);
|
||||
|
||||
$isCarousel = $this->format === ContentType::CAROUSEL_FORMAT;
|
||||
$agentFormat = $isCarousel ? 'carousel' : 'single';
|
||||
$agentFormat = $isCarousel ? GeneratorFormat::Carousel : GeneratorFormat::Single;
|
||||
$slideCount = $isCarousel && $this->imageCount > 0 ? $this->imageCount : 1;
|
||||
|
||||
$context = new TemplateContext(
|
||||
|
|
@ -88,8 +90,7 @@ public function handle(): void
|
|||
|
||||
$structured = $response->structured ?? [];
|
||||
|
||||
$humanizeFormat = $isCarousel ? 'carousel' : $agentFormat;
|
||||
$structured = $this->humanize($workspace, $structured, $humanizeFormat, $this->template);
|
||||
$structured = $this->humanize($workspace, $structured, $agentFormat, $style->style());
|
||||
|
||||
$generated = $style->assemble($structured, $context);
|
||||
$post = $this->createPostFromGenerated($workspace, $generated, $socialAccount);
|
||||
|
|
@ -116,14 +117,14 @@ public function handle(): void
|
|||
* @param array<string, mixed> $structured
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function humanize(Workspace $workspace, array $structured, string $format, string $styleKey = 'image_card'): array
|
||||
private function humanize(Workspace $workspace, array $structured, GeneratorFormat $format, ContentStyle $style): array
|
||||
{
|
||||
if ($styleKey === 'tweet_card' || $styleKey === 'tweet_card_image') {
|
||||
if (! $style->humanizes()) {
|
||||
return $structured;
|
||||
}
|
||||
|
||||
try {
|
||||
$input = $format === 'carousel'
|
||||
$input = $format->isCarousel()
|
||||
? [
|
||||
'caption' => data_get($structured, 'caption', ''),
|
||||
'slides' => array_map(
|
||||
|
|
@ -151,7 +152,7 @@ private function humanize(Workspace $workspace, array $structured, string $forma
|
|||
provider: (string) config('ai.default'),
|
||||
model: (string) config('ai.default_text_model'),
|
||||
userId: $this->userId,
|
||||
metadata: ['agent' => 'post_humanizer', 'format' => $format],
|
||||
metadata: ['agent' => 'post_humanizer', 'format' => $format->value],
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('PostContentHumanizer failed, using generator output as-is', [
|
||||
|
|
@ -162,7 +163,7 @@ private function humanize(Workspace $workspace, array $structured, string $forma
|
|||
return $structured;
|
||||
}
|
||||
|
||||
if ($format === 'carousel') {
|
||||
if ($format->isCarousel()) {
|
||||
$structured['caption'] = data_get($humanized, 'caption', $structured['caption'] ?? '');
|
||||
$originalSlides = $structured['slides'] ?? [];
|
||||
$humanizedSlides = data_get($humanized, 'slides', []);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
expect($t->style())->toBe(ContentStyle::ImageCard)
|
||||
->and($t->key())->toBe('image_card')
|
||||
->and($t->needsAccount())->toBeFalse()
|
||||
->and($t->generatorFormat())->toBe('single')
|
||||
->and($t->promptView($context))->toBe('prompts.post_content.generator')
|
||||
->and($t->supportedFormats())->toBe([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
expect($t->style())->toBe(ContentStyle::TweetCardImage)
|
||||
->and($t->key())->toBe('tweet_card_image')
|
||||
->and($t->needsAccount())->toBeTrue()
|
||||
->and($t->generatorFormat())->toBe('tweet_card')
|
||||
->and($t->style()->isTweetCard())->toBeTrue()
|
||||
->and($t->promptView($singleContext))->toBe('prompts.post_content.tweet_card_image')
|
||||
->and($t->promptView($carouselContext))->toBe('prompts.post_content.tweet_card_image_carousel');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
expect($t->style())->toBe(ContentStyle::TweetCard)
|
||||
->and($t->key())->toBe('tweet_card')
|
||||
->and($t->needsAccount())->toBeTrue()
|
||||
->and($t->generatorFormat())->toBe('tweet_card')
|
||||
->and($t->style()->isTweetCard())->toBeTrue()
|
||||
->and($t->promptView($singleContext))->toBe('prompts.post_content.tweet_card')
|
||||
->and($t->promptView($carouselContext))->toBe('prompts.post_content.tweet_card_carousel');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use App\Actions\Automation\Node\RunGenerateNode;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Models\Automation;
|
||||
use App\Models\User;
|
||||
|
|
@ -128,20 +129,20 @@
|
|||
|
||||
// tiktok_photo allows 35, but AI generation is capped at 10.
|
||||
expect($node->deriveFormat([['content_type' => 'tiktok_photo']], ['target_slide_count' => 15]))
|
||||
->toBe(['format' => 'carousel', 'slide_count' => 10]);
|
||||
->toBe(['format' => GeneratorFormat::Carousel, 'slide_count' => 10]);
|
||||
|
||||
// pinterest_carousel allows only 5.
|
||||
expect($node->deriveFormat([['content_type' => 'pinterest_carousel']], ['target_slide_count' => 8]))
|
||||
->toBe(['format' => 'carousel', 'slide_count' => 5]);
|
||||
->toBe(['format' => GeneratorFormat::Carousel, 'slide_count' => 5]);
|
||||
});
|
||||
|
||||
it('recognises facebook_post as multi-image capable (rules-derived, not a hardcoded list)', function () {
|
||||
$node = app(RunGenerateNode::class);
|
||||
|
||||
expect($node->deriveFormat([['content_type' => 'facebook_post']], ['target_slide_count' => 6]))
|
||||
->toBe(['format' => 'carousel', 'slide_count' => 6]);
|
||||
->toBe(['format' => GeneratorFormat::Carousel, 'slide_count' => 6]);
|
||||
|
||||
// A single-image (video) format never becomes a carousel.
|
||||
expect($node->deriveFormat([['content_type' => 'tiktok_video']], ['target_slide_count' => 5]))
|
||||
->toBe(['format' => 'single', 'slide_count' => 1]);
|
||||
->toBe(['format' => GeneratorFormat::Single, 'slide_count' => 1]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Ai\Agents\PostContentGenerator;
|
||||
use App\Ai\Agents\PostContentHumanizer;
|
||||
use App\Enums\Ai\ContentStyle;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Models\Automation;
|
||||
|
|
@ -180,7 +181,7 @@
|
|||
'target_slide_count' => 2,
|
||||
]);
|
||||
|
||||
PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->format === 'carousel'
|
||||
PostContentGenerator::assertPrompted(fn ($prompt) => $prompt->agent->format === GeneratorFormat::Carousel
|
||||
&& $prompt->agent->platformContext === ContentType::XPost->value);
|
||||
});
|
||||
|
||||
|
|
@ -260,7 +261,7 @@
|
|||
|
||||
['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 5]);
|
||||
|
||||
expect($format)->toBe('carousel');
|
||||
expect($format)->toBe(GeneratorFormat::Carousel);
|
||||
expect($slideCount)->toBe(5);
|
||||
});
|
||||
|
||||
|
|
@ -276,7 +277,7 @@
|
|||
|
||||
['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 1]);
|
||||
|
||||
expect($format)->toBe('single');
|
||||
expect($format)->toBe(GeneratorFormat::Single);
|
||||
expect($slideCount)->toBe(1);
|
||||
});
|
||||
|
||||
|
|
@ -290,7 +291,7 @@
|
|||
|
||||
['format' => $format, 'slide_count' => $slideCount] = $action->deriveFormat($accountsConfig, ['target_slide_count' => 5]);
|
||||
|
||||
expect($format)->toBe('single');
|
||||
expect($format)->toBe(GeneratorFormat::Single);
|
||||
expect($slideCount)->toBe(1);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use App\Ai\Agents\PostContentGenerator;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\JsonSchema\JsonSchemaTypeFactory;
|
||||
|
||||
|
|
@ -60,7 +61,7 @@
|
|||
|
||||
test('single format schema returns content and image_keywords', function () {
|
||||
$workspace = Workspace::factory()->make();
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: 'single');
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Single);
|
||||
|
||||
$schemaFactory = new JsonSchemaTypeFactory;
|
||||
$schema = $agent->schema($schemaFactory);
|
||||
|
|
@ -73,7 +74,7 @@
|
|||
|
||||
test('carousel format schema returns caption and slides', function () {
|
||||
$workspace = Workspace::factory()->make();
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: 'carousel', slideCount: 5);
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 5);
|
||||
|
||||
$schemaFactory = new JsonSchemaTypeFactory;
|
||||
$schema = $agent->schema($schemaFactory);
|
||||
|
|
@ -85,7 +86,7 @@
|
|||
|
||||
test('carousel slide schema includes role enum', function () {
|
||||
$workspace = Workspace::factory()->make();
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: 'carousel', slideCount: 4);
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 4);
|
||||
|
||||
$schemaFactory = new JsonSchemaTypeFactory;
|
||||
$schema = $agent->schema($schemaFactory);
|
||||
|
|
@ -100,7 +101,7 @@
|
|||
|
||||
test('carousel instructions mention slide count', function () {
|
||||
$workspace = Workspace::factory()->make();
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: 'carousel', slideCount: 5);
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 5);
|
||||
|
||||
$instructions = $agent->instructions();
|
||||
|
||||
|
|
@ -110,7 +111,7 @@
|
|||
|
||||
test('carousel instructions describe the roteiro arc with hook, development, proof and cta', function () {
|
||||
$workspace = Workspace::factory()->make();
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: 'carousel', slideCount: 4);
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Carousel, slideCount: 4);
|
||||
|
||||
$instructions = $agent->instructions();
|
||||
|
||||
|
|
@ -123,7 +124,7 @@
|
|||
|
||||
test('single format instructions do not include carousel roteiro section', function () {
|
||||
$workspace = Workspace::factory()->make();
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: 'single');
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Single);
|
||||
|
||||
$instructions = $agent->instructions();
|
||||
|
||||
|
|
@ -133,7 +134,7 @@
|
|||
|
||||
test('single instructions mention image_keywords', function () {
|
||||
$workspace = Workspace::factory()->make();
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: 'single');
|
||||
$agent = new PostContentGenerator(workspace: $workspace, format: GeneratorFormat::Single);
|
||||
|
||||
$instructions = $agent->instructions();
|
||||
|
||||
|
|
@ -143,7 +144,7 @@
|
|||
test('instructions include examples when platform context provided and templates exist', function () {
|
||||
$agent = new PostContentGenerator(
|
||||
workspace: Workspace::factory()->make(),
|
||||
format: 'carousel',
|
||||
format: GeneratorFormat::Carousel,
|
||||
slideCount: 3,
|
||||
platformContext: 'instagram_carousel',
|
||||
);
|
||||
|
|
@ -161,7 +162,7 @@
|
|||
test('instructions do not include examples section when platform context is null', function () {
|
||||
$agent = new PostContentGenerator(
|
||||
workspace: Workspace::factory()->make(),
|
||||
format: 'single',
|
||||
format: GeneratorFormat::Single,
|
||||
);
|
||||
|
||||
expect($agent->instructions())->not->toContain('curated library');
|
||||
|
|
@ -170,7 +171,7 @@
|
|||
test('instructions inject the platform character cap from the shared budget', function () {
|
||||
$agent = new PostContentGenerator(
|
||||
workspace: Workspace::factory()->make(),
|
||||
format: 'single',
|
||||
format: GeneratorFormat::Single,
|
||||
platformContext: 'x_post',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,12 @@
|
|||
expect(ContentStyle::TweetCardImage->needsAccount())->toBeTrue();
|
||||
});
|
||||
|
||||
test('humanizes is true only for ImageCard', function () {
|
||||
expect(ContentStyle::ImageCard->humanizes())->toBeTrue();
|
||||
expect(ContentStyle::TweetCard->humanizes())->toBeFalse();
|
||||
expect(ContentStyle::TweetCardImage->humanizes())->toBeFalse();
|
||||
});
|
||||
|
||||
test('previewAsset returns the correct public path for each case', function () {
|
||||
expect(ContentStyle::ImageCard->previewAsset())->toBe('/images/ai-templates/image-card.png');
|
||||
expect(ContentStyle::TweetCard->previewAsset())->toBe('/images/ai-templates/tweet-card.png');
|
||||
|
|
|
|||
Loading…
Reference in a new issue