diff --git a/app/Ai/Agents/SocialMediaAssistant.php b/app/Ai/Agents/SocialMediaAssistant.php index 0c1861e1..ba55e4fb 100644 --- a/app/Ai/Agents/SocialMediaAssistant.php +++ b/app/Ai/Agents/SocialMediaAssistant.php @@ -4,9 +4,12 @@ namespace App\Ai\Agents; +use App\Ai\PlatformRules\Contract; +use App\Ai\PlatformRules\Registry as PlatformRulesRegistry; use App\Ai\Tools\GenerateAudio; use App\Ai\Tools\GenerateImage; use App\Ai\Tools\GenerateVideo; +use App\Enums\SocialAccount\Platform; use App\Models\AiMessage; use App\Models\Post; use App\Models\Workspace; @@ -37,9 +40,31 @@ public function instructions(): string 'tone' => $this->workspace->brand_tone ?? 'professional', 'voice_notes' => $this->workspace->brand_voice_notes ?? '', 'locale' => app()->getLocale(), + 'platform_rules' => $this->activePlatformRules(), ])->render(); } + /** + * @return array + */ + private function activePlatformRules(): array + { + if (! $this->post) { + return []; + } + + $platforms = $this->post->postPlatforms + ->pluck('platform') + ->filter() + ->map(fn ($p) => $p instanceof Platform ? $p : Platform::tryFrom((string) $p)) + ->filter() + ->unique(fn (Platform $p) => $p->value) + ->values() + ->all(); + + return PlatformRulesRegistry::forMany($platforms); + } + /** * @return iterable */ diff --git a/resources/views/prompts/assistant/platforms.blade.php b/resources/views/prompts/assistant/platforms.blade.php new file mode 100644 index 00000000..7e7fd977 --- /dev/null +++ b/resources/views/prompts/assistant/platforms.blade.php @@ -0,0 +1,9 @@ +@if(count($platform_rules) > 0) +ACTIVE PLATFORMS FOR THIS POST: +The user is creating a post targeting the following platform(s). Tailor content length, format, and tone to these constraints. + +@foreach($platform_rules as $rule) +{{ $rule->summary() }} + +@endforeach +@endif diff --git a/resources/views/prompts/assistant/system.blade.php b/resources/views/prompts/assistant/system.blade.php index fcd8ed40..6d234d85 100644 --- a/resources/views/prompts/assistant/system.blade.php +++ b/resources/views/prompts/assistant/system.blade.php @@ -29,46 +29,7 @@ - You ONLY help with social media content creation. - If the user asks about anything unrelated, politely decline and redirect to content creation. -PLATFORM KNOWLEDGE: -You must understand each platform's formats, constraints, and best practices to create optimal content. - -Instagram: -- Feed Post: square (1:1) or portrait (4:5). Up to 10 images/videos. Caption max 2200 chars. 30 hashtags max (3-5 recommended). -- Carousel: same as Feed Post with 2-10 slides. All slides share the same aspect ratio. First slide is the hook. Great for educational/step-by-step content. -- Reel: vertical 9:16 only. Video only. Up to 90 seconds (30-60s performs best). Hook in first 1-3 seconds. -- Story: vertical 9:16. Single image or video. Disappears after 24h. Use CTAs and interactive elements. - -Facebook: -- Post: flexible aspect ratio. Up to 10 images. Caption up to 63,206 chars (but short posts perform better). Text-only allowed. -- Reel: vertical 9:16. Video only. Up to 90 seconds. Similar to Instagram Reels. -- Story: vertical 9:16. Single image or video. Disappears after 24h. - -X (Twitter): -- Post: max 280 characters. Up to 4 images or 1 video. Landscape 16:9 or square 1:1 for images. Concise, punchy copy. - -TikTok: -- Video: vertical 9:16 only. Video only. 15-60 seconds performs best. Hook in first 1-2 seconds. Trendy, authentic style. - -YouTube: -- Short: vertical 9:16. Video only. Up to 60 seconds. Loop-friendly content. Hook immediately. - -LinkedIn: -- Post: max 3000 chars. 1 image/video. First 2-3 lines visible before "see more" — make them compelling. Professional tone. -- Carousel: PDF-based document with swipeable slides. Up to 20 slides (8-12 optimal). Images only. Educational content performs extremely well. - -Threads: -- Post: max 500 chars. Up to 10 images/videos. Conversational tone. Ask questions to drive replies. - -Pinterest: -- Pin: portrait 2:3 (1000x1500px). Single image. Title max 100 chars, description max 500 chars. Use text overlay on images. -- Video Pin: vertical 9:16 or portrait 2:3. 4 seconds to 15 minutes. -- Carousel: portrait 2:3. Up to 5 images. Images only. - -Bluesky: -- Post: max 300 chars. Up to 4 images or 1 video. Similar to X but shorter. - -Mastodon: -- Post: max 500 chars. Up to 4 images or 1 video. +@include('prompts.assistant.platforms') CONTENT CREATION RULES: - When the user mentions a specific platform, tailor the content to that platform's constraints and best practices. diff --git a/tests/Feature/Ai/SocialMediaAssistantTest.php b/tests/Feature/Ai/SocialMediaAssistantTest.php index fd906dde..1f132bdc 100644 --- a/tests/Feature/Ai/SocialMediaAssistantTest.php +++ b/tests/Feature/Ai/SocialMediaAssistantTest.php @@ -3,10 +3,13 @@ declare(strict_types=1); use App\Ai\Agents\SocialMediaAssistant; +use App\Enums\SocialAccount\Platform; use App\Enums\User\Setup; use App\Enums\UserWorkspace\Role; use App\Models\AiMessage; use App\Models\Post; +use App\Models\PostPlatform; +use App\Models\SocialAccount; use App\Models\User; use App\Models\Workspace; use Laravel\Ai\Enums\Lab; @@ -97,6 +100,34 @@ expect($messages[0]->content)->toContain('[This assistant message attached: 2 image]'); }); +test('instructions include rules only for active post platforms', function () { + $socialAccount = SocialAccount::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'platform' => Platform::X, + ]); + + PostPlatform::factory()->create([ + 'post_id' => $this->post->id, + 'social_account_id' => $socialAccount->id, + 'platform' => Platform::X, + ]); + + $agent = new SocialMediaAssistant($this->workspace, $this->post->fresh('postPlatforms')); + + $instructions = $agent->instructions(); + + expect($instructions)->toContain('X (Twitter): 280 chars'); + expect($instructions)->not->toContain('Instagram: caption max 2200'); +}); + +test('instructions include no platform rules section when post has no platforms', function () { + $agent = new SocialMediaAssistant($this->workspace, $this->post); + + $instructions = $agent->instructions(); + + expect($instructions)->not->toContain('ACTIVE PLATFORMS FOR THIS POST'); +}); + test('provider honors trypost.ai.text_provider config', function () { config()->set('trypost.ai.text_provider', 'openai');