Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
32.63% covered (danger)
32.63%
264 / 809
18.18% covered (danger)
18.18%
4 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 2
FlyMsgAiParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
FlyMsgAIService
32.67% covered (danger)
32.67%
264 / 808
19.05% covered (danger)
19.05%
4 / 21
12040.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 rewrite
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 1
380
 checkSentences
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 1
156
 convertStringToJson
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 engageGenerate
62.36% covered (warning)
62.36%
111 / 178
0.00% covered (danger)
0.00%
0 / 1
237.40
 postGenerate
66.86% covered (warning)
66.86%
113 / 169
0.00% covered (danger)
0.00%
0 / 1
236.24
 watchVideo
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
12
 extractYouTubeId
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 readBlogUrl
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 parseHtmlContent
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 generate
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
56
 getPrompts
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 sendRequestToGeminiAPI
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 detectGeneratedAIResponseLanguage
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
2.01
 translateGeneratedPrompt
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 translateRequestHeader
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getUserPrompts
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 1
72
 transform
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 formattedPromptResponse
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 formatTrackingResponses
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getTrackingResponse
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Services\FlyMsgAI;
4
5use App\Filters\AIResponseFilter\Remove8Spaces;
6use App\Filters\AIResponseFilter\RemoveBackSlash;
7use App\Filters\AIResponseFilter\RemoveHorizontalRules;
8use App\Filters\AIResponseFilter\RemoveMarkdownBold;
9use App\Filters\AIResponseFilter\RemoveMarkdownHeaders;
10use App\Filters\AIResponseFilter\RemoveMarkdownLinks;
11use App\Filters\AIResponseFilter\RemoveMarkdownUnderlines;
12use App\Filters\AIResponseFilter\RemoveOutputPrefix;
13use App\Helpers\Constants;
14use App\Http\Models\FlyGrammarLanguage;
15use App\Http\Models\FlyMsgAI\FlyMsgAITracking;
16use App\Http\Models\FlyMsgAI\SavedPrompt;
17use App\Http\Models\PromptCompanyNewUpdate;
18use App\Http\Models\PromptLanguage;
19use App\Http\Models\PromptLengthOfPost;
20use App\Http\Models\PromptPersonalMilestone;
21use App\Http\Models\Prompts\CustomPrompts;
22use App\Http\Models\Prompts\PromptExample;
23use App\Http\Models\Prompts\PromptModel;
24use App\Http\Models\Prompts\PromptSetting;
25use App\Http\Models\Prompts\PromptType;
26use App\Http\Models\Prompts\YoutubeVideos;
27use App\Http\Models\PromptTone;
28use App\Http\Models\UserPersona;
29use App\Http\Services\AIPromptService;
30use App\Http\Services\NodeJsAIBridgeService;
31use App\Traits\Prompts\PromptTypeTrait;
32use Exception;
33use GuzzleHttp\Client;
34use GuzzleHttp\Exception\GuzzleException;
35use Illuminate\Http\Response;
36use Illuminate\Pipeline\Pipeline;
37use Illuminate\Support\Facades\Cache;
38use Illuminate\Support\Facades\Log;
39use Illuminate\Support\Str;
40use Symfony\Component\DomCrawler\Crawler;
41
42class FlyMsgAiParams
43{
44    public function __construct(
45        public readonly string $mission,
46        public readonly string $persona,
47        public readonly array $instructions,
48        public readonly array $constraints,
49        public readonly string $examples,
50        public readonly string $context,
51        public readonly ?string $trackingResponse = '',
52        public readonly ?string $existentContentInstructions = '',
53    ) {}
54}
55
56class FlyMsgAIService
57{
58    use PromptTypeTrait;
59
60    public function __construct(
61        private AIPromptService $aiPromptService,
62        private NodeJsAIBridgeService $bridge
63    ) {}
64
65    public function rewrite($data, ?string $userId = null, ?string $companyId = null)
66    {
67        $product = $data['product'];
68        $action = $data['action'];
69        $fullText = $data['context'] ?? '';
70        $input = $data['input'];
71
72        if (empty($input) || empty($action)) {
73            throw new Exception('Input and action are required', Response::HTTP_BAD_REQUEST);
74        }
75
76        $aiPrompt = $this->aiPromptService->getLatestByProductAndName($product, $action);
77
78        $prompt = "<intro>{$aiPrompt->context}</intro>\n";
79        $prompt .= "<mission>{$aiPrompt->mission}</mission>\n";
80        $prompt .= '<instructions>';
81        foreach ($aiPrompt->instructions as $index => $instruction) {
82            if ($index === 0) {
83                $prompt .= "{$instruction}\n";
84            } else {
85                $number = $index;
86                $prompt .= "{$number}{$instruction}\n";
87            }
88        }
89        $prompt .= "</instructions>\n";
90        $prompt .= '<constraints>';
91        foreach ($aiPrompt->constraints as $index => $constraint) {
92            if ($index === 0) {
93                $prompt .= "{$constraint}\n";
94            } else {
95                $number = $index;
96                $prompt .= "{$number}{$constraint}\n";
97            }
98        }
99        $prompt .= "</constraints>\n";
100        $prompt .= '<examples>';
101        foreach ($aiPrompt->examples as $index => $example) {
102            $number = $index + 1;
103            $exampleInput = $example['input'];
104            $exampleOutput = $example['output'];
105            $prompt .= "{Example {$number}}\n***Input: ***{$exampleInput}\n***Output: ***{$exampleOutput}\n";
106        }
107        $prompt .= "</examples>\n";
108
109        if (! empty($fullText)) {
110            // $prompt .= "<context>{$fullText}</context>\n";
111        }
112
113        switch ($action) {
114            case 'change-tone':
115                $tone = $data['tone'];
116                $tonePrompt = PromptTone::where('id', $tone)->first();
117                $prompt .= "<input>{$input}</input><tone>{$tonePrompt->name} - {$tonePrompt->prompt}</tone>\n";
118                break;
119            case 'translate':
120                $language = $data['language'];
121                $languagePrompt = FlyGrammarLanguage::where('id', $language)->first();
122                $prompt .= "<input>{$input}</input><language>{$languagePrompt->description} - {$languagePrompt->value}</language>\n";
123                break;
124            case 'humanize':
125            case 'make-shorter':
126            case 'make-longer':
127            case 'simplify':
128            case 'continue-writing':
129            case 'improve-writing':
130            default:
131                $prompt .= "<input>{$input}</input>\n";
132                break;
133        }
134
135        $prompt .= '***Output: ***';
136
137        $response = $this->sendRequestToGeminiAPI(
138            $prompt,
139            'en',
140            $aiPrompt->tokens,
141            $aiPrompt->temperature,
142            $aiPrompt->model,
143            $aiPrompt->top_p,
144            false,
145            null,
146            0,
147            ['feature' => 'rewrite', 'user_id' => $userId, 'company_id' => $companyId, 'context' => $data['context'] ?? null]
148        );
149
150        Log::info("FLYWRITE {$aiPrompt->product} - {$aiPrompt->name} - {$aiPrompt->version}", [
151            'input' => $data,
152            'output' => $response,
153            'prompt' => [
154                'prompt' => $prompt,
155                'input' => $input,
156            ],
157            'config' => $aiPrompt,
158        ]);
159
160        $output = $this->convertStringToJson(trim(str_replace('OUTPUT:', ' ', $response)));
161
162        if (is_string($output)) {
163            $output = [
164                'suggestion' => $output,
165            ];
166        }
167
168        return [
169            'prompt' => $prompt,
170            'output' => $output,
171        ];
172    }
173
174    public function checkSentences($sentences, $force = false)
175    {
176        $cachedSentences = [];
177        $newSentences = [];
178        foreach ($sentences as $sentence) {
179            $md5 = md5($sentence);
180
181            if ($force) {
182                Cache::forget("flywrite_sentence_{$md5}");
183            }
184
185            if (Cache::has("flywrite_sentence_{$md5}")) {
186                $cachedSentences[] = Cache::get("flywrite_sentence_{$md5}");
187            } else {
188                $newSentences[] = $sentence;
189            }
190        }
191
192        $aiPrompt = $this->aiPromptService->getLatestByProductAndName('sentence_rewrite', 'score');
193
194        if (empty($newSentences)) {
195            return [
196                'sentences' => $cachedSentences,
197                'threshold' => $aiPrompt->threshold,
198            ];
199        }
200
201        $prompt = "<mission>{$aiPrompt->mission}</mission>\n";
202        $prompt .= "<context>{$aiPrompt->context}</context>\n";
203        $prompt .= '<instructions>';
204        foreach ($aiPrompt->instructions as $index => $instruction) {
205            if ($index === 0) {
206                $prompt .= "{$instruction}\n";
207            } else {
208                $number = $index + 1;
209                $prompt .= "{$number}{$instruction}\n";
210            }
211        }
212        $prompt .= "</instructions>\n";
213        $prompt .= '<constraints>';
214        foreach ($aiPrompt->constraints as $index => $constraint) {
215            if ($index === 0) {
216                $prompt .= "{$constraint}\n";
217            } else {
218                $number = $index + 1;
219                $prompt .= "{$number}{$constraint}\n";
220            }
221        }
222        $prompt .= "</constraints>\n";
223        $prompt .= '<examples>';
224        foreach ($aiPrompt->examples as $index => $example) {
225            $number = $index + 1;
226            $exampleInput = $example['input'];
227            $exampleOutput = $example['output'];
228            $prompt .= "{Example {$number}}\n***Input: ***{$exampleInput}\n***Output: ***{$exampleOutput}\n";
229        }
230        $prompt .= "</examples>\n";
231
232        $newSentences = array_map(function ($sentence, $index) {
233            $number = $index + 1;
234
235            return "{$number}{$sentence}";
236        }, $newSentences, array_keys($newSentences));
237        $newSentences = implode("\n", $newSentences);
238        $prompt .= "<input>\n{$newSentences}\n</input>";
239        $prompt .= '***Output: ***';
240
241        $response = $this->sendRequestToGeminiAPI(
242            $prompt,
243            'en',
244            $aiPrompt->tokens,
245            $aiPrompt->temperature,
246            $aiPrompt->model,
247            $aiPrompt->top_p,
248            false,
249            null,
250            0,
251            ['feature' => 'check_sentences']
252        );
253
254        Log::info("FLYWRITE {$aiPrompt->product} - {$aiPrompt->name} - {$aiPrompt->version}", [
255            'input' => $newSentences,
256            'output' => $response,
257            'prompt' => $prompt,
258            'config' => $aiPrompt,
259        ]);
260
261        $result = trim($response);
262
263        $validatedSentences = $this->convertStringToJson($result);
264
265        $validatedSentences = is_array($validatedSentences) ? $validatedSentences : [];
266        foreach ($validatedSentences as $sentence) {
267            // cache sentences
268            $md5 = md5($sentence['sentence']);
269            Cache::put("flywrite_sentence_{$md5}", $sentence, 60 * 60 * 10); // cache for 10 hours
270        }
271
272        return [
273            'sentences' => array_merge($cachedSentences, $validatedSentences),
274            'threshold' => $aiPrompt->threshold,
275        ];
276    }
277
278    private function convertStringToJson(string $input)
279    {
280        $cleanedInput = Str::of($input)
281            ->replaceMatches('/^```json/m', '')
282            ->replaceMatches('/```$/m', '')
283            ->trim();
284
285        try {
286            return json_decode($cleanedInput, true, 512, JSON_THROW_ON_ERROR);
287        } catch (\Exception $e) {
288            return $input;
289        }
290    }
291
292    /**
293     * Generate an engage response using AI.
294     *
295     * @param  string  $promptId  The AIPrompts ID
296     * @param  array  $context  The post context data
297     * @param  string  $uniqueId  Unique identifier for tracking
298     * @param  mixed  $user  The authenticated user
299     * @param  bool  $useHashtags  Whether to include hashtags
300     * @param  bool  $useEmojis  Whether to include emojis
301     * @param  string|null  $personaId  UserPersona ID
302     * @param  string|null  $toneId  PromptTone ID
303     * @param  string|null  $additionalMission  Additional instructions
304     * @param  string|null  $customPromptId  CustomPrompts ID
305     * @param  bool  $is_a_regenerate_request  Whether this is a regeneration
306     * @param  bool  $isReply  Whether this is a reply
307     * @param  bool  $dryRun  If true, return assembled prompt without calling AI API
308     * @return array{prompt: string, response: string|null, config?: array}
309     */
310    public function engageGenerate(
311        string $promptId,
312        $context,
313        $uniqueId,
314        $user,
315        bool $useHashtags,
316        bool $useEmojis,
317        ?string $personaId = null,
318        ?string $toneId = null,
319        ?string $additionalMission = null,
320        ?string $customPromptId = null,
321        $is_a_regenerate_request = false,
322        $isReply = false,
323        bool $dryRun = false
324    ) {
325        $aiPrompt = $this->aiPromptService->getById($promptId);
326        $name = strtolower($aiPrompt->name);
327
328        $customPrompt = ! empty($customPromptId) ? CustomPrompts::find($customPromptId) : null;
329        $tone = ! empty($toneId) ? PromptTone::find($toneId) : null;
330
331        $userPersona = ! empty($personaId) ? UserPersona::where('_id', $personaId)->first() : null;
332        if (! empty($userPersona) && $userPersona->prompt_tone) {
333            $tone = $userPersona->prompt_tone;
334        }
335
336        if (empty($userPersona)) {
337            $defaultUserPersona = UserPersona::where('user_id', $user->id)->where('is_default', true)->first();
338            if (! empty($defaultUserPersona)) {
339                $userPersona = $defaultUserPersona;
340            }
341        }
342
343        if (! empty($customPrompt) && ! empty($customPrompt->template_prompt_id)) {
344            $aiPrompt = $this->aiPromptService->getById($customPrompt->template_prompt_id);
345        }
346
347        $prompt = $aiPrompt->context;
348
349        if (! empty($customPrompt) && ! empty($customPrompt->persona_id) && empty($userPersona)) {
350            $userPersona = UserPersona::where('_id', $customPrompt->persona_id)->first();
351        }
352
353        $persona = ! empty($userPersona) ? $userPersona->ai_emulation : $aiPrompt->persona;
354
355        if (empty($additionalMission) && ! empty($customPrompt)) {
356            $additionalMission = $customPrompt->additional_instructions ?? '';
357        }
358
359        $prompt = "{$prompt}\n<persona>{$persona}</persona>";
360        $mission = $aiPrompt->mission;
361        if ($name === 'custom') {
362            $mission = str_replace('{{CUSTOM_PROMPT}}', $mission, $additionalMission);
363        }
364        if (! empty($additionalMission) && $name !== 'custom') {
365            $mission = "{$mission}\n{$additionalMission}";
366        }
367
368        $tracking_responses = $this->getTrackingResponse($user->id, $name, 'flyengage', $uniqueId);
369
370        $existentContent = $is_a_regenerate_request && count($tracking_responses) > 0 ? $aiPrompt->existent_content_instructions : '';
371
372        if (! empty($existentContent)) {
373            $mission = "{$mission}\n**Regenerate**: {$existentContent}";
374        }
375
376        $prompt = "{$prompt}\n<mission>{$mission}</mission>";
377
378        if (! empty($customPrompt) && ! empty($customPrompt->prompt_tone_id) && empty($tone)) {
379            $tone = PromptTone::find($customPrompt->prompt_tone_id);
380        }
381
382        if (! empty($tone)) {
383            $prompt = "{$prompt}\n<tone_of_voice>Write in the following tone of voice:{$tone->prompt}</tone_of_voice>";
384        }
385
386        $instructions = '';
387        $instructionsCount = 0;
388        foreach ($aiPrompt->instructions as $instruction) {
389            if ($instructionsCount === 0) {
390                $instructions .= "{$instruction}\n";
391            } else {
392                $instructions .= "{$instructionsCount}{$instruction}\n";
393            }
394            $instructionsCount++;
395        }
396
397        $constraints = '';
398        $constraintsCount = 0;
399        foreach ($aiPrompt->constraints as $constraint) {
400            if ($constraintsCount === 0) {
401                $constraints .= "{$constraint}\n";
402            } else {
403                $constraints .= "{$constraintsCount}{$constraint}\n";
404            }
405            $constraintsCount++;
406        }
407
408        if ($useHashtags) {
409            $instructions = "{$instructions}\n{$instructionsCount}{$aiPrompt->include_hashtags_prompt}";
410            $instructionsCount++;
411        } else {
412            $constraints = "{$constraints}\n{$constraintsCount}{$aiPrompt->exclude_hashtags_prompt}";
413            $constraintsCount++;
414        }
415
416        if ($useEmojis) {
417            $instructions = "{$instructions}\n{$instructionsCount}{$aiPrompt->include_emojis_prompt}";
418            $instructionsCount++;
419        } else {
420            $constraints = "{$constraints}\n{$constraintsCount}{$aiPrompt->exclude_emojis_prompt}";
421            $constraintsCount++;
422        }
423
424        if ($is_a_regenerate_request && count($tracking_responses) > 0) {
425            foreach ($aiPrompt->existent_content_constraints as $constraint) {
426                $constraints = "{$constraints}\n{$constraintsCount}{$constraint}";
427                $constraintsCount++;
428            }
429        }
430
431        if ($isReply) {
432            $instructions = "{$instructions}\n{$instructionsCount}. Reply to this specific comment or reply provided.";
433        }
434
435        $prompt = "{$prompt}\n<instructions>{$instructions}</instructions>";
436        $prompt = "{$prompt}\n<constraints>{$constraints}</constraints>";
437
438        $existentContentCount = 1;
439        if (! empty($existentContent)) {
440            foreach ($tracking_responses as $response) {
441                $existentContent = "{$existentContent}\n{$existentContentCount}.\n<existent_content>{$response}</existent_content>\n";
442                $existentContentCount++;
443            }
444
445            $prompt = "{$prompt}\n<existent_contents>{$existentContent}</existent_contents>";
446        }
447
448        // examples
449        if (! empty($aiPrompt->examples)) {
450            $prompt .= '<examples>';
451            $exampleCount = 1;
452            foreach ($aiPrompt->examples as $example) {
453                $exampleInput = $example['input'];
454                $exampleOutput = $example['output'];
455                if (! empty($exampleInput) || ! empty($exampleOutput)) {
456                    $prompt .= "{Example {$exampleCount}}\n***Input: ***{$exampleInput}\n***Output: ***{$exampleOutput}\n";
457                    $exampleCount++;
458                }
459            }
460            $prompt .= "</examples>\n";
461        }
462
463        $author = $context['post']['postedBy'] ?? null;
464        $authorDescription = $context['post']['description'] ?? null;
465        $postedTime = $context['post']['postedTime'] ?? null;
466        $postContent = $context['post']['content'] ?? null;
467
468        $prompt = "{$prompt}\n<input>";
469        if (! empty($author)) {
470            $prompt = "{$prompt}\n**Author**: {$author}";
471        }
472        if (! empty($authorDescription)) {
473            $prompt = "{$prompt}\n**Author Description**: {$authorDescription}";
474        }
475        if (! empty($postedTime)) {
476            $prompt = "{$prompt}\n**Posted Time**: {$postedTime}";
477        }
478        $prompt = "{$prompt}\n**Post**: {$postContent}";
479
480        if ($isReply) {
481            $comments = $context['comments'] ?? [];
482            $comments = array_filter($comments, function ($comment) {
483                return $comment['isReplyingTo'] ?? false;
484            });
485            if (count($comments) > 0) {
486                $prompt = "{$prompt}\n**Comment**:";
487                $comments = array_map(function ($comment) {
488                    $output = '**Comment**:';
489                    $replies = $comment['replies'] ?? [];
490                    $repliesTrue = array_filter($replies, function ($reply) {
491                        return $reply['isReplyingTo'] ?? false;
492                    });
493                    if (count($replies) == 0 || count($repliesTrue) == 0) {
494                        $output = "{$output}\nReply to this specific comment.";
495                    }
496
497                    $author = $comment['commenterName'] ?? '';
498                    $authorUrl = $comment['commenterUrl'] ?? '';
499                    $commentText = $comment['commentText'] ?? '';
500
501                    if (! empty($author)) {
502                        $output = "{$output}\nAuthor: {$author}";
503                    }
504                    if (! empty($authorUrl)) {
505                        $output = "{$output}\nAuthor URL: {$authorUrl}";
506                    }
507                    if (! empty($commentText)) {
508                        $output = "{$output}\nComment: {$commentText}";
509                    }
510
511                    if (count($replies) > 0 && count($repliesTrue) > 0) {
512                        $output = "{$output}\nReplies:";
513
514                        $repliesTrue = array_filter($replies, function ($reply) {
515                            return $reply['isReplyingTo'] ?? false;
516                        });
517
518                        $repliesTrue = array_map(function ($reply, $index) {
519                            $author = $reply['commenterName'] ?? '';
520                            $authorUrl = $reply['commenterUrl'] ?? '';
521                            $commentText = $reply['commentText'] ?? '';
522
523                            $result = ($index + 1).'. ';
524                            if (! empty($author)) {
525                                $result = "{$result}\nAuthor: {$author}";
526                            }
527                            if (! empty($authorUrl)) {
528                                $result = "{$result}\nAuthor URL: {$authorUrl}";
529                            }
530                            if (! empty($commentText)) {
531                                $result = "{$result}\nComment: {$commentText}";
532                            }
533
534                            $result = "{$result} Reply to this specific reply.";
535
536                            return $result;
537                        }, $repliesTrue, array_keys($repliesTrue));
538
539                        $repliesTrue = implode("\n", $repliesTrue);
540                        $output = "{$output}\n{$repliesTrue}";
541                    }
542
543                    return $output;
544                }, $comments);
545
546                $prompt = "{$prompt}\n".implode("\n", $comments);
547            }
548        }
549
550        $prompt = "{$prompt}\n</input>";
551
552        $prompt = "{$prompt}\nOutput:";
553
554        $language = $this->detectGeneratedAIResponseLanguage($postContent);
555
556        if ($dryRun) {
557            return [
558                'prompt' => $prompt,
559                'response' => null,
560                'config' => [
561                    'model' => $aiPrompt->model,
562                    'temperature' => $aiPrompt->temperature,
563                    'max_tokens' => $aiPrompt->tokens,
564                    'top_p' => $aiPrompt->top_p,
565                ],
566            ];
567        }
568
569        $response = $this->sendRequestToGeminiAPI(
570            $prompt,
571            $language,
572            $aiPrompt->tokens,
573            $aiPrompt->temperature,
574            $aiPrompt->model,
575            $aiPrompt->top_p,
576            $aiPrompt->is_grounding,
577            null,
578            0,
579            ['feature' => 'engage_generate', 'user_id' => $user->id ?? null, 'company_id' => $user->company_id ?? null]
580        );
581
582        $cleanResponse = $this->transform($response);
583
584        Log::info('FlyMSG AI: ', [
585            'prompt' => $prompt,
586            'response' => $cleanResponse,
587            'ai_prompt' => $aiPrompt,
588        ]);
589
590        return [
591            'prompt' => $prompt,
592            'response' => $cleanResponse,
593        ];
594    }
595
596    /**
597     * Generate a post response using AI.
598     *
599     * @param  string  $promptId  The AIPrompts ID
600     * @param  string|null  $youtube_url  YouTube URL for video-based posts
601     * @param  string|null  $blog_url  Blog URL for article-based posts
602     * @param  string  $uniqueId  Unique identifier for tracking
603     * @param  mixed  $user  The authenticated user
604     * @param  bool  $useHashtags  Whether to include hashtags
605     * @param  bool  $useEmojis  Whether to include emojis
606     * @param  string  $promptLanguageId  PromptLanguage ID
607     * @param  string  $lengthOfPostId  PromptLengthOfPost ID
608     * @param  string|null  $topic  Topic for thought leadership posts
609     * @param  string|null  $insert_role  Role for hiring posts
610     * @param  string|null  $promptCompanyNewUpdateId  PromptCompanyNewUpdate ID
611     * @param  string|null  $promptPersonalMilestoneId  PromptPersonalMilestone ID
612     * @param  string|null  $personaId  UserPersona ID
613     * @param  string|null  $toneId  PromptTone ID
614     * @param  string|null  $additionalMission  Additional instructions
615     * @param  string|null  $customPromptId  CustomPrompts ID
616     * @param  bool  $is_a_regenerate_request  Whether this is a regeneration
617     * @param  bool  $dryRun  If true, return assembled prompt without calling AI API
618     * @return array{prompt: string, response: string|null, config?: array}
619     */
620    public function postGenerate(
621        string $promptId,
622        $youtube_url,
623        $blog_url,
624        $uniqueId,
625        $user,
626        bool $useHashtags,
627        bool $useEmojis,
628        string $promptLanguageId,
629        string $lengthOfPostId,
630        ?string $topic,
631        ?string $insert_role,
632        ?string $promptCompanyNewUpdateId = null,
633        ?string $promptPersonalMilestoneId = null,
634        ?string $personaId = null,
635        ?string $toneId = null,
636        ?string $additionalMission = null,
637        ?string $customPromptId = null,
638        $is_a_regenerate_request = false,
639        bool $dryRun = false
640    ) {
641        $aiPrompt = $this->aiPromptService->getById($promptId);
642        $name = strtolower($aiPrompt->name);
643
644        $language = PromptLanguage::find($promptLanguageId);
645        $lengthOfPost = PromptLengthOfPost::find($lengthOfPostId);
646        $customPrompt = ! empty($customPromptId) ? CustomPrompts::find($customPromptId) : null;
647        $tone = ! empty($toneId) ? PromptTone::find($toneId) : null;
648        $promptCompanyNewUpdate = ! empty($promptCompanyNewUpdateId) ? PromptCompanyNewUpdate::find($promptCompanyNewUpdateId) : null;
649        $promptPersonalMilestone = ! empty($promptPersonalMilestoneId) ? PromptPersonalMilestone::find($promptPersonalMilestoneId) : null;
650
651        $userPersona = ! empty($personaId) ? UserPersona::where('_id', $personaId)->first() : null;
652        if (! empty($userPersona) && $userPersona->prompt_tone) {
653            $tone = $userPersona->prompt_tone;
654        }
655
656        if (empty($userPersona)) {
657            $defaultUserPersona = UserPersona::where('user_id', $user->id)->where('is_default', true)->first();
658            if (! empty($defaultUserPersona)) {
659                $userPersona = $defaultUserPersona;
660            }
661        }
662
663        if (! empty($customPrompt) && ! empty($customPrompt->template_prompt_id)) {
664            $aiPrompt = $this->aiPromptService->getById($customPrompt->template_prompt_id);
665        }
666
667        $prompt = $aiPrompt->context;
668
669        if (! empty($youtube_url) || ! empty($blog_url)) {
670            $prompt = "{$prompt}\nYou will receive a URL and detailed instructions on crafting the perfect post.";
671        }
672
673        if (! empty($customPrompt) && ! empty($customPrompt->persona_id) && empty($userPersona)) {
674            $userPersona = UserPersona::where('_id', $customPrompt->persona_id);
675        }
676
677        $persona = ! empty($userPersona) ? $userPersona->ai_emulation : $aiPrompt->persona;
678        $prompt = "{$prompt}\n<persona>{$persona}</persona>";
679
680        $mission = $aiPrompt->mission;
681
682        if ($name == 'thought leadership') {
683            $mission = str_replace('{{topic}}', $topic, $mission);
684        } elseif ($name == 'company news') {
685            $mission = str_replace('{{promptCompanyNewUpdate}}', $promptCompanyNewUpdate->prompt, $mission);
686        } elseif ($name == 'celebrate something') {
687            $mission = str_replace('{{promptPersonalMilestone}}', $promptPersonalMilestone->prompt, $mission);
688        } elseif ($name == 'hiring') {
689            $mission = str_replace('{{insert_role}}', $insert_role, $mission);
690        } elseif ($name === 'custom') {
691            $mission = str_replace('{{CUSTOM_PROMPT}}', $mission, $additionalMission);
692        }
693
694        if (! empty($youtube_url)) {
695            $mission = $aiPrompt->youtube_url_mission;
696        } elseif (! empty($blog_url)) {
697            $mission = $aiPrompt->blog_url_mission;
698        }
699
700        if (! empty($additionalMission) && $name !== 'custom') {
701            $mission = "{$mission}\n{$additionalMission}";
702        }
703
704        if (! empty($youtube_url)) {
705            $uniqueId = $this->extractYouTubeId($youtube_url);
706        }
707
708        if (! empty($blog_url)) {
709            $uniqueId = $blog_url;
710        }
711
712        $tracking_responses = $this->getTrackingResponse($user->id, $name, 'flypost', $uniqueId);
713
714        $existentContent = $is_a_regenerate_request && count($tracking_responses) > 0 ? $aiPrompt->existent_content_instructions : '';
715
716        if (! empty($existentContent)) {
717            $mission = "{$mission}\n**Regenerate**: {$existentContent}";
718        }
719
720        $prompt = "{$prompt}\n<mission>{$mission}</mission>";
721
722        if (! empty($youtube_url)) {
723            try {
724                $youtubeVideo = $this->watchVideo($youtube_url, $name);
725                if (empty($youtubeVideo)) {
726                    throw new Exception('It was not possible to read the content of the video. Please, provide a valid URL.');
727                }
728                $youtubeSummary = $youtubeVideo->result;
729            } catch (Exception $e) {
730                $youtubeSummary = 'It was not possible to read the content of the video. Please, provide a valid URL.';
731            }
732
733            $prompt = "{$prompt}\n**Youtube URL: {$youtube_url}**\n<youtube_video_summary>{$youtubeSummary}</youtube_video_summary>";
734        }
735
736        if (! empty($blog_url)) {
737            try {
738                $article = $this->readBlogUrl($blog_url);
739            } catch (Exception $e) {
740                $article = 'It was not possible to read the content of the blog. Please, provide a valid URL.';
741            }
742
743            $prompt = "{$prompt}\n**Blog URL: {$blog_url}**\n<scraped_content>{$article}</scraped_content>";
744        }
745
746        if (! empty($customPrompt) && ! empty($customPrompt->prompt_tone_id) && empty($tone)) {
747            $tone = PromptTone::find($customPrompt->prompt_tone_id);
748        }
749
750        if (! empty($tone)) {
751            $prompt = "{$prompt}\n<tone_of_voice>Write in the following tone of voice:{$tone->prompt}</tone_of_voice>";
752        }
753
754        $instructions = '';
755        $instructionsCount = 0;
756        foreach ($aiPrompt->instructions as $instruction) {
757            if ($instructionsCount === 0) {
758                $instructions .= "{$instruction}\n";
759            } else {
760                $instructions .= "{$instructionsCount}{$instruction}\n";
761            }
762            $instructionsCount++;
763        }
764
765        if (! empty($youtube_url)) {
766            foreach ($aiPrompt->youtube_url_instructions as $instruction) {
767                $instructions = "{$instructions}\n{$instructionsCount}{$instruction}";
768                $instructionsCount++;
769            }
770        }
771
772        foreach ($aiPrompt->instructions as $instruction) {
773            $instructions = "{$instructions}\n{$instructionsCount}{$instruction}";
774            $instructionsCount++;
775        }
776
777        if (! empty($blog_url)) {
778            foreach ($aiPrompt->blog_url_instructions as $instruction) {
779                $instructions = "{$instructions}\n{$instructionsCount}{$instruction}";
780                $instructionsCount++;
781            }
782        }
783
784        $constraints = 'Here are some constraints to consider when creating a social media post:';
785        $constraintsCount = 0;
786        foreach ($aiPrompt->constraints as $constraint) {
787            if ($constraintsCount === 0) {
788                $constraints .= "{$constraint}\n";
789            } else {
790                $constraints .= "{$constraintsCount}{$constraint}\n";
791            }
792            $constraintsCount++;
793        }
794
795        if ($useHashtags) {
796            $instructions = "{$instructions}\n{$instructionsCount}{$aiPrompt->include_hashtags_prompt}";
797            $instructionsCount++;
798        } else {
799            $constraints = "{$constraints}\n{$constraintsCount}{$aiPrompt->exclude_hashtags_prompt}";
800            $constraintsCount++;
801        }
802
803        if ($useEmojis) {
804            $instructions = "{$instructions}\n{$instructionsCount}{$aiPrompt->include_emojis_prompt}";
805            $instructionsCount++;
806        } else {
807            $constraints = "{$constraints}\n{$constraintsCount}{$aiPrompt->exclude_emojis_prompt}";
808            $constraintsCount++;
809        }
810
811        if ($is_a_regenerate_request && count($tracking_responses) > 0) {
812            foreach ($aiPrompt->existent_content_constraints as $constraint) {
813                $constraints = "{$constraints}\n{$constraintsCount}{$constraint}";
814                $constraintsCount++;
815            }
816        }
817
818        $prompt = "{$prompt}\n<instructions>{$instructions}</instructions>";
819        $prompt = "{$prompt}\n<constraints>{$constraints}</constraints>";
820
821        $existentContentCount = 1;
822        if (! empty($existentContent)) {
823            foreach ($tracking_responses as $response) {
824                $existentContent = "{$existentContent}\n{$existentContentCount}.\n<existent_content>{$response}</existent_content>\n";
825                $existentContentCount++;
826            }
827
828            $prompt = "{$prompt}\n<existent_contents>{$existentContent}</existent_contents>";
829        }
830
831        $prompt .= '<examples>';
832        $exampleCount = 1;
833        $lenghtPostExamples = array_filter($aiPrompt->examples, function ($example) use ($lengthOfPost) {
834            return $example['length'] == $lengthOfPost->name;
835        });
836
837        foreach ($lenghtPostExamples as $example) {
838            $exampleHasHashtags = $example['hashtags'] ?? false;
839            $exampleHasEmojis = $example['emojis'] ?? false;
840            $exampleLength = $example['length'] ?? 'Medium (191 â€“ 300 words)';
841            if ($lengthOfPost->name !== $exampleLength) {
842                continue;
843            }
844
845            if (($useHashtags && $useEmojis && ! $exampleHasHashtags && ! $exampleHasEmojis) || (! $useHashtags && ! $useEmojis && $exampleHasHashtags && $exampleHasEmojis)) {
846                continue;
847            }
848
849            $exampleInput = $example['input'];
850            $exampleOutput = $example['output'];
851            $prompt .= "{Example {$exampleCount}}\n***Input: ***{$exampleInput}\n***Output: ***{$exampleOutput}\n";
852            $exampleCount++;
853        }
854        $prompt .= "</examples>\n";
855
856        $prompt = "{$prompt}\nOutput:\n1. Output format equals plain text.";
857        $prompt = "{$prompt}\n2. The length of this social media post should be: {$lengthOfPost->prompt}";
858        $prompt = "{$prompt}\n3. Output language should be: {$language->prompt}";
859        $prompt = "{$prompt}\n4. Provide the post without additional introductory content. Meticulously follow this rule.";
860
861        if ($dryRun) {
862            return [
863                'prompt' => $prompt,
864                'response' => null,
865                'config' => [
866                    'model' => $aiPrompt->model,
867                    'temperature' => $aiPrompt->temperature,
868                    'max_tokens' => $aiPrompt->tokens,
869                    'top_p' => $aiPrompt->top_p,
870                ],
871            ];
872        }
873
874        $response = $this->sendRequestToGeminiAPI(
875            $prompt,
876            // $language->prompt,
877            'en',
878            $aiPrompt->tokens,
879            $aiPrompt->temperature,
880            $aiPrompt->model,
881            $aiPrompt->top_p,
882            $aiPrompt->is_grounding,
883            null,
884            0,
885            ['feature' => 'post_generate', 'user_id' => $user->id ?? null, 'company_id' => $user->company_id ?? null]
886        );
887
888        $cleanResponse = $this->transform($response);
889
890        Log::info('FlyMSG AI: ', [
891            'prompt' => $prompt,
892            'response' => $cleanResponse,
893            'ai_prompt' => $aiPrompt,
894        ]);
895
896        return [
897            'prompt' => $prompt,
898            'response' => $cleanResponse,
899        ];
900    }
901
902    public function watchVideo($youtube_url, $name)
903    {
904        $youtubeId = $this->extractYouTubeId($youtube_url);
905
906        $youtubeVideo = YoutubeVideos::where('video_id', $youtubeId)->first();
907
908        if (! empty($youtubeVideo)) {
909            return $youtubeVideo;
910        }
911
912        // set the execution timeout to 5 minutes
913        ini_set('max_execution_time', '300');
914
915        $model = PromptModel::where('is_active', true)->latest()->first();
916        $setting = PromptSetting::where('prompt_model_id', $model->id)->where('is_active', true)->where('feature', 'flypost')->latest()->first();
917        $promptTypeM = PromptType::where('prompt_setting_id', $setting->id)->where('is_active', true)->where('feature', 'flypost')->where('name', $name)->first();
918
919        $prompt = $promptTypeM->youtube_extract_mission;
920
921        $instructionCount = 1;
922
923        foreach ($promptTypeM->youtube_extract_instructions as $instruction) {
924            $prompt = "{$prompt}\n{$instructionCount}{$instruction}";
925            $instructionCount++;
926        }
927
928        $prompt = "{$prompt}\n{$promptTypeM->youtube_extract_output}";
929
930        $response = $this->sendRequestToGeminiAPI(
931            $prompt,
932            'en',
933            $setting->output_token_limit,
934            $promptTypeM->youtube_extract_temperature,
935            $model->name,
936            $promptTypeM->youtube_extract_top_p,
937            $setting->is_grounding,
938            $youtube_url,
939            0,
940            ['feature' => 'youtube_watch']
941        );
942
943        Log::info('Youtube Summarization AI: ', [
944            'prompt' => $prompt,
945            'max_tokens' => $setting->output_token_limit,
946            'temperature' => $promptTypeM->youtube_extract_temperature,
947            'model' => $model->name,
948            'top_p' => $promptTypeM->youtube_extract_top_p,
949            'response' => $response,
950        ]);
951
952        return YoutubeVideos::create([
953            'video_id' => $youtubeId,
954            'prompt' => $prompt,
955            'result' => trim(str_replace('OUTPUT:', ' ', $response)),
956        ]);
957    }
958
959    private function extractYouTubeId($url)
960    {
961        $pattern = '%(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([A-Za-z0-9_-]{11})%i';
962
963        if (preg_match($pattern, $url, $matches)) {
964            return $matches[1];
965        }
966
967        return $url;
968    }
969
970    private function readBlogUrl($url)
971    {
972        $client = new Client;
973        $response = $client->request('GET', $url);
974        $html = $response->getBody()->getContents();
975
976        $crawler = new Crawler($html);
977        $parsedText = $this->parseHtmlContent($crawler->filter('body')->text());
978
979        Log::info('Scrapping URL: ', [
980            'url' => $url,
981            'text' => $parsedText,
982        ]);
983
984        return $parsedText;
985    }
986
987    private function parseHtmlContent($html_content)
988    {
989        $soup = new Crawler($html_content);
990
991        $soup->filter('script, style')->each(function ($node) {
992            $node->getNode(0)->parentNode->removeChild($node->getNode(0));
993        });
994
995        $texts = $soup->filter('p')->each(function ($node) {
996            return $node->text();
997        });
998
999        $cleaned_text = implode("\n", array_filter($texts));
1000
1001        $max_size = 60000;
1002        if (strlen($cleaned_text) > $max_size) {
1003            $cleaned_text = substr($cleaned_text, 0, $max_size);
1004        }
1005
1006        return $cleaned_text;
1007    }
1008
1009    /**
1010     * @return string
1011     *
1012     * @throws GuzzleException
1013     * @throws Exception
1014     */
1015    public function generate($name, $context, $prompt, $feature, $uniqueId, $is_a_regenerate_request = false)
1016    {
1017        $name = strtolower($name);
1018        $context = strip_tags($context);
1019        $userId = auth()->user()->id;
1020        // $promptType = $this->getPromptByFeatureAndName($feature, $name)->first();
1021        $model = PromptModel::where('is_active', true)->latest()->first();
1022
1023        $setting = PromptSetting::where('prompt_model_id', $model->id)->where('is_active', true)->where('feature', $feature)->latest()->first();
1024
1025        $promptTypeM = PromptType::where('prompt_setting_id', $setting->id)->where('is_active', true)->where('feature', $feature)->where('name', $name)->first();
1026
1027        // $promptExamples = collect($promptType->promptExamples);
1028        $promptExamples = PromptExample::where('prompt_type_id', $promptTypeM->id)->where('is_active', true)->get();
1029        $formattedPromptExamples = $this->formattedPromptResponse($promptExamples);
1030
1031        $validateCustomInstruction = $feature === 'flypost' || $feature === 'flyengage' && $name === 'custom' ? true : false;
1032
1033        $promptReplaced = str_replace('{{CUSTOM_PROMPT}}', $prompt, $prompt);
1034
1035        $tracking_responses = $this->getTrackingResponse($userId, $name, $feature, $uniqueId);
1036        $flymsgAIParams = new FlyMsgAiParams(
1037            mission: $validateCustomInstruction || ! empty($prompt) ? $promptReplaced : $promptTypeM->mission,
1038            persona: $promptTypeM->persona ?? '',
1039            instructions: $promptTypeM->instructions,
1040            constraints: $promptTypeM->constraints,
1041            examples: $formattedPromptExamples,
1042            context: $context,
1043            trackingResponse: $is_a_regenerate_request ? $this->formatTrackingResponses($tracking_responses) : '',
1044            existentContentInstructions: $promptTypeM->existent_content_instructions,
1045        );
1046
1047        $prompt = $this->getPrompts($flymsgAIParams);
1048
1049        $language = $this->detectGeneratedAIResponseLanguage($context);
1050
1051        Log::info('FlyMSG AI: ', [
1052            'prompt' => $prompt,
1053            'language' => $language,
1054            'max_tokens' => $setting->output_token_limit,
1055            'temperature' => $setting->temperature,
1056            'model' => $model->name,
1057            'top_p' => $setting->top_p,
1058        ]);
1059
1060        $response = $this->sendRequestToGeminiAPI(
1061            $prompt,
1062            $language,
1063            $setting->output_token_limit,
1064            $setting->temperature,
1065            $model->name,
1066            $setting->top_p,
1067            false,
1068            null,
1069            0,
1070            ['feature' => $feature, 'user_id' => $userId ?? null, 'company_id' => auth()->user()->company_id ?? null]
1071        );
1072
1073        return [
1074            'prompt' => $prompt,
1075            'response' => trim(str_replace('OUTPUT:', ' ', $response)),
1076        ];
1077    }
1078
1079    private function getPrompts(FlyMsgAiParams $params)
1080    {
1081        $formattedInstructions = array_reduce($params->instructions, function ($carry, $instruction) {
1082            return $carry.'<INSTRUCTION>'.$instruction.'</INSTRUCTION>';
1083        }, '');
1084        $formattedConstraints = array_reduce($params->constraints, function ($carry, $constraint) {
1085            return $carry.'<CONSTRAINT>'.$constraint.'</CONSTRAINT>';
1086        }, '');
1087
1088        $existentContentInstructions = empty($params->trackingResponse) ? '' : "$params->existentContentInstructions
1089        <EXISTENT_RESPONSES>
1090            $params->trackingResponse
1091        </EXISTENT_RESPONSES>";
1092
1093        return "$params->persona
1094        Follow the examples below:
1095        <EXAMPLES>
1096            $params->examples
1097        </EXAMPLES>
1098
1099        Now it's your turn!
1100
1101        <DOCUMENT>
1102            $params->context
1103        </DOCUMENT>
1104
1105        <INSTRUCTIONS>
1106            $formattedInstructions
1107        </INSTRUCTIONS>
1108
1109        <CONSTRAINTS>
1110            $formattedConstraints
1111        </CONSTRAINTS>
1112
1113        $existentContentInstructions
1114
1115        <QUERY>$params->mission</QUERY>
1116
1117        OUTPUT:";
1118    }
1119
1120    /**
1121     * Send a prompt to the AI via the Node.js bridge service.
1122     *
1123     * @param  string  $prompt  The full assembled prompt text
1124     * @param  string  $language  Target language code (e.g. 'en', 'es')
1125     * @param  int  $max_tokens  Maximum output tokens
1126     * @param  float  $temperature  Generation temperature
1127     * @param  string  $model  AI model identifier
1128     * @param  float  $topP  Top-P sampling value
1129     * @param  bool  $enableGoogleSearch  Whether to enable Google Search grounding
1130     * @param  string|null  $youtubeUrl  Optional YouTube video URI
1131     * @param  int  $thinkingBudget  Thinking token budget (0 = disabled)
1132     * @param  array  $metadata  Logging metadata (feature, user_id, company_id, context)
1133     * @return string The raw AI-generated text
1134     *
1135     * @throws Exception
1136     */
1137    protected function sendRequestToGeminiAPI(
1138        $prompt,
1139        $language,
1140        $max_tokens,
1141        $temperature,
1142        $model = 'gemini-2.5-flash',
1143        $topP = 1.0,
1144        $enableGoogleSearch = false,
1145        $youtubeUrl = null,
1146        $thinkingBudget = 0,
1147        array $metadata = [],
1148    ): string {
1149        // Normalise model name: strip legacy ':streamGenerateContent' suffix
1150        $normalisedModel = str_replace(':streamGenerateContent', '', $model);
1151
1152        $payload = [
1153            'provider' => 'vertex',
1154            'model' => $normalisedModel,
1155            'prompt' => $prompt,
1156            'config' => [
1157                'maxOutputTokens' => (int) $max_tokens,
1158                'temperature' => (float) $temperature,
1159                'topP' => (float) $topP,
1160                'thinkingBudget' => (int) $thinkingBudget,
1161                'enableGoogleSearch' => (bool) $enableGoogleSearch,
1162            ],
1163            'youtubeUrl' => $youtubeUrl ?: null,
1164        ];
1165
1166        $generatedResponse = $this->bridge->generate($payload, $metadata);
1167
1168        if ($language !== 'en') {
1169            $generatedResponse = $this->translateGeneratedPrompt($language, $generatedResponse);
1170        }
1171
1172        return $generatedResponse;
1173    }
1174
1175    /**
1176     * Detect the language
1177     *
1178     * @param  $textToBeTranslated
1179     * @return mixed|void
1180     *
1181     * @throws GuzzleException
1182     */
1183    public function detectGeneratedAIResponseLanguage($textToBeDetected)
1184    {
1185        try {
1186            $access_token = GoogleTranslate::getGoogleTranslateAccessToken();
1187            $client = new Client;
1188            $baseURL = 'https://translate.googleapis.com/v3beta1/projects/project-romeo/locations/global:detectLanguage';
1189            $data = [
1190                'content' => $textToBeDetected,
1191            ];
1192            $response = $client->post($baseURL, [
1193                'headers' => $this->translateRequestHeader($access_token),
1194                'json' => $data,
1195            ]);
1196            $response = json_decode($response->getBody()->getContents(), true);
1197
1198            return $response['languages'][0]['languageCode'];
1199        } catch (Exception $e) {
1200            Log::error($e->getMessage());
1201
1202            return 'en';
1203        }
1204    }
1205
1206    /**
1207     * Translate the generated generated response
1208     *
1209     * @return mixed|void
1210     *
1211     * @throws GuzzleException
1212     */
1213    private function translateGeneratedPrompt(string $detectedLanguage, string $textToTranslate)
1214    {
1215        try {
1216            $access_token = GoogleTranslate::getGoogleTranslateAccessToken();
1217            $client = new Client;
1218
1219            $baseURL = 'https://translation.googleapis.com/v3/projects/project-romeo:translateText';
1220
1221            $data = [
1222                'sourceLanguageCode' => 'en',
1223                'targetLanguageCode' => $detectedLanguage,
1224                'contents' => [$textToTranslate],
1225                'mimeType' => 'text/plain',
1226            ];
1227
1228            $response = $client->post($baseURL, [
1229                'headers' => $this->translateRequestHeader($access_token),
1230                'json' => $data,
1231            ]);
1232
1233            $response = json_decode($response->getBody()->getContents(), true);
1234
1235            return $response['translations'][0]['translatedText'];
1236        } catch (Exception $e) {
1237            Log::error($e);
1238
1239            return $textToTranslate;
1240        }
1241    }
1242
1243    /**
1244     * Return formatted Google Translate headers
1245     */
1246    private function translateRequestHeader($access_token): array
1247    {
1248        return [
1249            'Authorization' => "Bearer $access_token",
1250            'Content-Type' => 'application/json',
1251        ];
1252    }
1253
1254    public static function getUserPrompts($user, $feature)
1255    {
1256        $saved_prompts = SavedPrompt::where('user_id', $user->id)
1257            ->where('feature', $feature)
1258            ->latest()
1259            ->get()
1260            ->take(5);
1261
1262        $order = [];
1263
1264        if (str_contains($feature, 'flyengage')) {
1265            if (count($saved_prompts) < 4) {
1266                $model = PromptModel::where('is_active', true)->latest()->first();
1267                $setting = PromptSetting::where('prompt_model_id', $model->id)->where('is_active', true)->where('feature', $feature)->latest()->first();
1268                $prompts = PromptType::where('prompt_setting_id', $setting->id)->where('is_active', true)->where('feature', $feature)->get();
1269
1270                foreach ($prompts as $prompt) {
1271                    $user->saved_prompts()->updateOrCreate(
1272                        ['user_id' => $user->id, 'name' => $prompt->name, 'feature' => $feature],
1273                        [
1274                            'name' => $prompt->name,
1275                            'prompt' => $prompt->mission ?? '',
1276                            'feature' => $feature,
1277                        ]
1278                    );
1279                }
1280                $saved_prompts = SavedPrompt::where('user_id', $user->id)
1281                    ->where('feature', $feature)
1282                    ->latest()
1283                    ->get()
1284                    ->take(4);
1285            }
1286
1287            $order = ['curious', 'optimistic', 'thoughtful', 'custom'];
1288        } elseif ($feature == 'flypost') {
1289            if (count($saved_prompts) < 5) {
1290                $thought_leadership_prompt = $user->saved_prompts()->updateOrCreate(
1291                    ['user_id' => $user->id, 'name' => 'thought leadership', 'feature' => $feature],
1292                    [
1293                        'name' => 'thought leadership',
1294                        'prompt' => Constants::DEFAULT_FLYPOST_THOUGHT_LEADERSHIP_PROMPT,
1295                        'feature' => $feature,
1296                    ]
1297                );
1298
1299                $company_news_prompt = $user->saved_prompts()->updateOrCreate(
1300                    ['user_id' => $user->id, 'name' => 'company news', 'feature' => $feature],
1301                    [
1302                        'name' => 'company news',
1303                        'prompt' => Constants::DEFAULT_FLYPOST_COMPANY_NEWS_PROMPT,
1304                        'feature' => $feature,
1305                    ]
1306                );
1307
1308                $celebrate_something_prompt = $user->saved_prompts()->updateOrCreate(
1309                    ['user_id' => $user->id, 'name' => 'celebrate something', 'feature' => $feature],
1310                    [
1311                        'name' => 'celebrate something',
1312                        'prompt' => Constants::DEFAULT_FLYPOST_CELEBRATE_SOMETHING_PROMPT,
1313                        'feature' => $feature,
1314                    ]
1315                );
1316
1317                $hiring_prompt = $user->saved_prompts()->updateOrCreate(
1318                    ['user_id' => $user->id, 'name' => 'hiring', 'feature' => $feature],
1319                    [
1320                        'name' => 'hiring',
1321                        'prompt' => Constants::DEFAULT_FLYPOST_HIRING_PROMPT,
1322                        'feature' => $feature,
1323                    ]
1324                );
1325
1326                $custom_prompt = $user->saved_prompts()->updateOrCreate(
1327                    ['user_id' => $user->id, 'name' => 'custom', 'feature' => $feature],
1328                    [
1329                        'name' => 'custom',
1330                        'prompt' => Constants::DEFAULT_FLYPOST_CUSTOM_PROMPT,
1331                        'feature' => $feature,
1332                    ]
1333                );
1334
1335                $saved_prompts = [
1336                    $thought_leadership_prompt,
1337                    $company_news_prompt,
1338                    $celebrate_something_prompt,
1339                    $hiring_prompt,
1340                    $custom_prompt,
1341                ];
1342            }
1343
1344            $order = ['thought leadership', 'company news', 'celebrate something', 'hiring', 'custom'];
1345        } else {
1346            $saved_prompts = [];
1347        }
1348
1349        $saved_prompts = collect($saved_prompts)->sort(function ($a, $b) use ($order) {
1350            $posA = array_search($a->name, $order);
1351            $posB = array_search($b->name, $order);
1352
1353            $posA = $posA === false ? count($order) : $posA;
1354            $posB = $posB === false ? count($order) : $posB;
1355
1356            return $posA - $posB;
1357        });
1358
1359        $saved_prompts = $saved_prompts->values();
1360
1361        return $saved_prompts;
1362    }
1363
1364    public function transform(string $data): string
1365    {
1366        $pipelines = [
1367            RemoveOutputPrefix::class,
1368            RemoveMarkdownBold::class,
1369            RemoveMarkdownHeaders::class,
1370            RemoveMarkdownUnderlines::class,
1371            RemoveMarkdownLinks::class,
1372            RemoveHorizontalRules::class,
1373            Remove8Spaces::class,
1374            RemoveBackSlash::class,
1375        ];
1376
1377        return app(Pipeline::class)
1378            ->send($data)
1379            ->through($pipelines)
1380            ->thenReturn();
1381    }
1382
1383    private function formattedPromptResponse($promptExamples)
1384    {
1385        return $promptExamples->reduce(function ($carry, $item) {
1386            $input = $item->input;
1387            $output = $item->output;
1388
1389            return $carry."\n<EXAMPLE>\n<INPUT>\n$input\n</INPUT>\n<OUTPUT>\n$output\n</OUTPUT>\n</EXAMPLE>";
1390        }, '');
1391    }
1392
1393    private function formatTrackingResponses($promptTrackingResponse)
1394    {
1395        return "<EXISTENT_RESPONSES>\n".
1396            array_reduce($promptTrackingResponse, fn ($carry, $response) => $carry.'<EXISTENT_RESPONSE>'.$response.'</EXISTENT_RESPONSE>'."\n", '').
1397            '</EXISTENT_RESPONSES>';
1398    }
1399
1400    private function getTrackingResponse($userId, $name, $feature, $uniqueId)
1401    {
1402        return FlyMsgAITracking::where('user_id', $userId)
1403            ->where('feature', $feature)
1404            ->where('button', $name)
1405            ->where('unique_id', $uniqueId)
1406            ->latest()
1407            ->take(5)
1408            ->pluck('prompt_response')
1409            ->toArray();
1410    }
1411}