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