Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
32.63% |
264 / 809 |
|
18.18% |
4 / 22 |
CRAP | |
0.00% |
0 / 2 |
| FlyMsgAiParams | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| FlyMsgAIService | |
32.67% |
264 / 808 |
|
19.05% |
4 / 21 |
12040.91 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| rewrite | |
0.00% |
0 / 80 |
|
0.00% |
0 / 1 |
380 | |||
| checkSentences | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
156 | |||
| convertStringToJson | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| engageGenerate | |
62.36% |
111 / 178 |
|
0.00% |
0 / 1 |
237.40 | |||
| postGenerate | |
66.86% |
113 / 169 |
|
0.00% |
0 / 1 |
236.24 | |||
| watchVideo | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
12 | |||
| extractYouTubeId | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| readBlogUrl | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
| parseHtmlContent | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
| generate | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
56 | |||
| getPrompts | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
| sendRequestToGeminiAPI | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
| detectGeneratedAIResponseLanguage | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
2.01 | |||
| translateGeneratedPrompt | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
| translateRequestHeader | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| getUserPrompts | |
0.00% |
0 / 86 |
|
0.00% |
0 / 1 |
72 | |||
| transform | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
| formattedPromptResponse | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
| formatTrackingResponses | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| getTrackingResponse | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services\FlyMsgAI; |
| 4 | |
| 5 | use App\Filters\AIResponseFilter\Remove8Spaces; |
| 6 | use App\Filters\AIResponseFilter\RemoveBackSlash; |
| 7 | use App\Filters\AIResponseFilter\RemoveHorizontalRules; |
| 8 | use App\Filters\AIResponseFilter\RemoveMarkdownBold; |
| 9 | use App\Filters\AIResponseFilter\RemoveMarkdownHeaders; |
| 10 | use App\Filters\AIResponseFilter\RemoveMarkdownLinks; |
| 11 | use App\Filters\AIResponseFilter\RemoveMarkdownUnderlines; |
| 12 | use App\Filters\AIResponseFilter\RemoveOutputPrefix; |
| 13 | use App\Helpers\Constants; |
| 14 | use App\Http\Models\FlyGrammarLanguage; |
| 15 | use App\Http\Models\FlyMsgAI\FlyMsgAITracking; |
| 16 | use App\Http\Models\FlyMsgAI\SavedPrompt; |
| 17 | use App\Http\Models\PromptCompanyNewUpdate; |
| 18 | use App\Http\Models\PromptLanguage; |
| 19 | use App\Http\Models\PromptLengthOfPost; |
| 20 | use App\Http\Models\PromptPersonalMilestone; |
| 21 | use App\Http\Models\Prompts\CustomPrompts; |
| 22 | use App\Http\Models\Prompts\PromptExample; |
| 23 | use App\Http\Models\Prompts\PromptModel; |
| 24 | use App\Http\Models\Prompts\PromptSetting; |
| 25 | use App\Http\Models\Prompts\PromptType; |
| 26 | use App\Http\Models\Prompts\YoutubeVideos; |
| 27 | use App\Http\Models\PromptTone; |
| 28 | use App\Http\Models\UserPersona; |
| 29 | use App\Http\Services\AIPromptService; |
| 30 | use App\Http\Services\NodeJsAIBridgeService; |
| 31 | use App\Traits\Prompts\PromptTypeTrait; |
| 32 | use Exception; |
| 33 | use GuzzleHttp\Client; |
| 34 | use GuzzleHttp\Exception\GuzzleException; |
| 35 | use Illuminate\Http\Response; |
| 36 | use Illuminate\Pipeline\Pipeline; |
| 37 | use Illuminate\Support\Facades\Cache; |
| 38 | use Illuminate\Support\Facades\Log; |
| 39 | use Illuminate\Support\Str; |
| 40 | use Symfony\Component\DomCrawler\Crawler; |
| 41 | |
| 42 | class 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 | |
| 56 | class 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 | } |