diff --git a/app/Ai/Agents/SocialMediaAssistant.php b/app/Ai/Agents/SocialMediaAssistant.php new file mode 100644 index 00000000..75c26f25 --- /dev/null +++ b/app/Ai/Agents/SocialMediaAssistant.php @@ -0,0 +1,80 @@ + $this->workspace->name ?? '', + 'brand_description' => $this->workspace->brand_description ?? '', + 'brand_website' => $this->workspace->brand_website ?? '', + 'tone' => $this->workspace->brand_tone ?? 'professional', + 'voice_notes' => $this->workspace->brand_voice_notes ?? '', + 'locale' => app()->getLocale(), + ])->render(); + } + + /** + * @return iterable + */ + public function messages(): iterable + { + if (! $this->post) { + return []; + } + + return AiMessage::query() + ->where('post_id', $this->post->id) + ->whereIn('role', ['user', 'assistant']) + ->oldest() + ->limit(20) + ->get() + ->map(fn (AiMessage $m) => new Message($m->role, $this->enrichContent($m))) + ->all(); + } + + public function provider(): Lab + { + return match (config('trypost.ai.text_provider')) { + 'openai' => Lab::OpenAI, + default => Lab::Gemini, + }; + } + + private function enrichContent(AiMessage $m): string + { + $content = $m->content; + + if ($m->role === 'assistant' && ! empty($m->attachments)) { + $counts = collect($m->attachments) + ->groupBy('type') + ->map(fn ($group, $type) => count($group)." {$type}") + ->implode(', '); + + $content .= "\n\n[This assistant message attached: {$counts}]"; + } + + return $content; + } +} diff --git a/tests/Feature/Ai/SocialMediaAssistantTest.php b/tests/Feature/Ai/SocialMediaAssistantTest.php new file mode 100644 index 00000000..fd906dde --- /dev/null +++ b/tests/Feature/Ai/SocialMediaAssistantTest.php @@ -0,0 +1,108 @@ +user = User::factory()->create(['setup' => Setup::Completed]); + $this->workspace = Workspace::factory()->create([ + 'user_id' => $this->user->id, + 'name' => 'Paulo Coffee', + 'brand_tone' => 'friendly', + ]); + $this->workspace->members()->attach($this->user->id, ['role' => Role::Member->value]); + $this->user->update(['current_workspace_id' => $this->workspace->id]); + + $this->post = Post::factory()->create([ + 'workspace_id' => $this->workspace->id, + 'user_id' => $this->user->id, + ]); +}); + +test('instructions include brand context from workspace', function () { + $agent = new SocialMediaAssistant($this->workspace, $this->post); + + $instructions = $agent->instructions(); + + expect($instructions) + ->toContain('Paulo Coffee') + ->toContain('friendly'); +}); + +test('messages returns AiMessage rows as SDK messages', function () { + AiMessage::factory()->create([ + 'post_id' => $this->post->id, + 'role' => 'user', + 'content' => 'Write a caption', + ]); + AiMessage::factory()->create([ + 'post_id' => $this->post->id, + 'role' => 'assistant', + 'content' => 'Here it is!', + ]); + + $agent = new SocialMediaAssistant($this->workspace, $this->post); + + $messages = collect($agent->messages())->all(); + + expect($messages)->toHaveCount(2); + expect($messages[0])->toBeInstanceOf(Message::class); + expect($messages[0]->role->value)->toBe('user'); + expect($messages[0]->content)->toBe('Write a caption'); + expect($messages[1]->role->value)->toBe('assistant'); + expect($messages[1]->content)->toBe('Here it is!'); +}); + +test('messages is empty when no post is provided', function () { + $agent = new SocialMediaAssistant($this->workspace); + + $messages = collect($agent->messages())->all(); + + expect($messages)->toBeEmpty(); +}); + +test('messages is empty when post has no AiMessages', function () { + $agent = new SocialMediaAssistant($this->workspace, $this->post); + + $messages = collect($agent->messages())->all(); + + expect($messages)->toBeEmpty(); +}); + +test('messages enriches assistant content with attachment summary', function () { + AiMessage::factory()->create([ + 'post_id' => $this->post->id, + 'role' => 'assistant', + 'content' => 'Here is your image', + 'attachments' => [ + ['id' => 'a', 'type' => 'image'], + ['id' => 'b', 'type' => 'image'], + ], + ]); + + $agent = new SocialMediaAssistant($this->workspace, $this->post); + + $messages = collect($agent->messages())->all(); + + expect($messages[0]->content)->toContain('Here is your image'); + expect($messages[0]->content)->toContain('[This assistant message attached: 2 image]'); +}); + +test('provider honors trypost.ai.text_provider config', function () { + config()->set('trypost.ai.text_provider', 'openai'); + + expect((new SocialMediaAssistant($this->workspace))->provider())->toBe(Lab::OpenAI); + + config()->set('trypost.ai.text_provider', 'gemini'); + + expect((new SocialMediaAssistant($this->workspace))->provider())->toBe(Lab::Gemini); +});