feat: inject active post platform rules into agent instructions

SocialMediaAssistant::activePlatformRules() reads post_platforms on the
attached Post, maps them to Platform enum values, and resolves rule
classes via Registry::forMany(). The rendered instructions include only
the summaries of platforms actually selected on this post — so when a
user is posting to X only, the agent doesn't waste tokens thinking
about Instagram Reels.

The PLATFORM KNOWLEDGE block in system.blade.php is replaced with
@include('prompts.assistant.platforms') which iterates the passed rules
and renders each summary. When the post has no platforms, the section
renders nothing (a @if guards it).
This commit is contained in:
Paulo Castellano 2026-04-16 10:04:26 -03:00
parent 0d1ca71380
commit 43c4cc6df0
4 changed files with 66 additions and 40 deletions

View file

@ -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<int, Contract>
*/
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<Message>
*/

View file

@ -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

View file

@ -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.

View file

@ -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');