Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 224 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
FlyMsgAIController | |
0.00% |
0 / 224 |
|
0.00% |
0 / 9 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkQuota | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
6 | |||
savePrompt | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
6 | |||
generate | |
0.00% |
0 / 68 |
|
0.00% |
0 / 1 |
30 | |||
updatePrompt | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
deletePrompt | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
12 | |||
getGoogleToken | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
resetPrompt | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
30 | |||
getTypingSpeedConfigByFeature | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | namespace App\Http\Controllers\v1\FlyMsgAI; |
4 | |
5 | use Carbon\Carbon; |
6 | use ErrorException; |
7 | use App\Helpers\Constants; |
8 | use Illuminate\Http\Request; |
9 | use App\Services\FlyMsgAI\GeminiAPI; |
10 | use App\Traits\SubscriptionTrait; |
11 | use Illuminate\Http\JsonResponse; |
12 | use App\Http\Controllers\Controller; |
13 | use App\Events\TrackFlyMsgAIUsageEvent; |
14 | use App\Helpers\FlyMSGLogger; |
15 | use App\Http\Models\Parameter; |
16 | use App\Http\Models\FlyMsgAI\SavedPrompt; |
17 | use App\Http\Models\Prompts\PromptModel; |
18 | use App\Http\Models\Prompts\PromptSetting; |
19 | use App\Http\Models\Prompts\PromptType; |
20 | use App\Http\Models\Setting; |
21 | use App\Http\Requests\UpdatePromptRequest; |
22 | use App\Http\Services\SavedPromptsService; |
23 | use App\Services\FlyMsgAI\FlyMsgAIService; |
24 | use App\Services\FlyMsgAI\GoogleTranslate; |
25 | use App\Traits\AccountCenter\Reporting\FlyMsgAITrackingTrait; |
26 | use Illuminate\Http\Response; |
27 | use Illuminate\Support\Facades\Log; |
28 | |
29 | class FlyMsgAIController extends Controller |
30 | { |
31 | use SubscriptionTrait, FlyMsgAITrackingTrait; |
32 | |
33 | public function __construct( |
34 | private SavedPromptsService $savedPromptsService |
35 | ) {} |
36 | |
37 | /** |
38 | * Get a users AI prompt usage. |
39 | * |
40 | * @param Request $request |
41 | * @return JsonResponse |
42 | */ |
43 | public function checkQuota(Request $request): JsonResponse |
44 | { |
45 | $feature = $request->feature; |
46 | |
47 | if (!$feature) { |
48 | return response()->json([ |
49 | 'status' => 'error', |
50 | 'message' => 'Feature is required.', |
51 | ], 400); |
52 | } |
53 | |
54 | $user = $request->user(); |
55 | |
56 | $currentSubscriptionPlan = $this->getCurrentPlan($user); |
57 | $total = Constants::CURRENT_SUBSCRIPTION_PLAN_IDENTIFIERS[$currentSubscriptionPlan->identifier]; |
58 | |
59 | $used = $this->getQuotaUsed($user->id); |
60 | |
61 | $quota = [ |
62 | 'used' => $used, |
63 | 'total' => $total, |
64 | 'remaining' => max($total - $used, 0), |
65 | 'depleted' => $used >= $total, |
66 | ]; |
67 | |
68 | // Retrieve saved prompts |
69 | $saved_prompts = FlyMsgAIService::getUserPrompts($user, $feature); |
70 | |
71 | return response()->json([ |
72 | 'status' => 'success', |
73 | 'data' => [ |
74 | 'quota' => $quota, |
75 | 'saved_prompts' => $saved_prompts, |
76 | ], |
77 | ]); |
78 | } |
79 | |
80 | |
81 | /** |
82 | * Save a prompt to the database. |
83 | * |
84 | * @param Request $request |
85 | * @return JsonResponse |
86 | */ |
87 | public function savePrompt(Request $request): JsonResponse |
88 | { |
89 | try { |
90 | $data = $request->validate([ |
91 | 'name' => 'required|max:100|string|unique:saved_prompts,name,NULL,id,user_id,' . $request->user()->id, |
92 | 'prompt' => 'required|string', |
93 | 'feature' => 'required|string', |
94 | ]); |
95 | |
96 | $saved_prompt = $request->user()->saved_prompts() |
97 | ->updateOrCreate( |
98 | ['name' => $data['name'], 'feature' => $data['feature']], |
99 | ['prompt' => $data['prompt']] |
100 | ); |
101 | |
102 | return response()->json([ |
103 | 'status' => 'success', |
104 | 'data' => [ |
105 | 'prompt' => $saved_prompt, |
106 | ], |
107 | ]); |
108 | } catch (\Throwable $th) { |
109 | FlyMSGLogger::logError("savePrompt", $th); |
110 | return response()->json([ |
111 | 'status' => 'error', |
112 | 'message' => 'The given data failed validation or something went wrong.' . $th->getMessage(), |
113 | ], 400); |
114 | } |
115 | } |
116 | |
117 | /** |
118 | * Generates a response to a prompt. This uses a waterfall approach |
119 | * of retrying the reqsuest until it is successful. |
120 | * |
121 | * Here is an example: Where we hit a rate limit with OpenAI, we will retry the request |
122 | * to another model. If that fails for rate limit again, we go to the next. |
123 | * If all of OpenAI models are maxed out, we will retry the request to the Google GeminiAPI api |
124 | * and repeat the cycle again. |
125 | * |
126 | * |
127 | * @param Request $request |
128 | * |
129 | * @return JsonResponse |
130 | */ |
131 | public function generate(Request $request): JsonResponse |
132 | { |
133 | $user = $request->user(); |
134 | |
135 | $currentSubscriptionPlan = $this->getCurrentPlan($user); |
136 | $total = Constants::CURRENT_SUBSCRIPTION_PLAN_IDENTIFIERS[$currentSubscriptionPlan->identifier]; |
137 | |
138 | $used = $this->getQuotaUsed($user->id); |
139 | |
140 | if ($used >= $total) { |
141 | return response()->json([ |
142 | 'status' => 'error', |
143 | 'message' => 'You have used up your quota for the day. Please upgrade your plan.', |
144 | ], Response::HTTP_FORBIDDEN); |
145 | } |
146 | |
147 | $request->validate([ |
148 | 'prompt' => 'required|string', |
149 | 'context' => 'sometimes|string', |
150 | 'name' => 'required|string', |
151 | 'feature' => 'required|string|in:flyengage,flypost', |
152 | 'is_regenerate' => 'sometimes|boolean', |
153 | 'uniqueId' => 'sometimes|nullable|string', |
154 | ], [ |
155 | 'feature.in' => 'The feature field must be either flyengage or flypost.', |
156 | ]); |
157 | |
158 | $name = $request->name; |
159 | $prompt = $request->prompt; |
160 | $feature = $request->feature; |
161 | $context = strip_tags($request->context); |
162 | $is_regenerate = $request->is_regenerate; |
163 | $uniqueId = null; |
164 | |
165 | if ($feature == 'flyengage') { |
166 | $uniqueId = "https://www.linkedin.com/feed/update/{$request->uniqueId}"; |
167 | } elseif ($feature == 'flypost') { |
168 | $uniqueId = str_slug($request->uniqueId); |
169 | } |
170 | |
171 | try { |
172 | $ai_service = new FlyMsgAIService(); |
173 | $aiResult = $ai_service->generate($name, strip_tags($context), $prompt, $feature, $uniqueId, $is_regenerate); |
174 | $prompt_response = $ai_service->transform($aiResult['response']); |
175 | |
176 | TrackFlyMsgAIUsageEvent::dispatch( |
177 | $user, |
178 | $prompt_response, |
179 | $request->browser, |
180 | $request->name, |
181 | $request->prompt . "\n\n Post:\n\n " . $request->context, |
182 | $request->feature, |
183 | $uniqueId, |
184 | $aiResult, |
185 | null |
186 | ); |
187 | |
188 | $used++; |
189 | |
190 | $quota = [ |
191 | 'used' => $used, |
192 | 'total' => $total, |
193 | 'remaining' => max($total - $used, 0), |
194 | 'depleted' => $used >= $total, |
195 | ]; |
196 | |
197 | $words_per_minute_typing_speed_control = $this->getTypingSpeedConfigByFeature($request->feature, $user); |
198 | |
199 | return response()->json([ |
200 | 'status' => 'success', |
201 | 'data' => [ |
202 | 'prompt' => $request->prompt . "\n\n Post:\n\n " . $request->context, |
203 | 'prompt_response' => $prompt_response, |
204 | 'quota' => $quota, |
205 | 'typingConfig' => $words_per_minute_typing_speed_control, |
206 | ], |
207 | ]); |
208 | } catch (\Throwable $th) { |
209 | FlyMSGLogger::logError("generate", $th); |
210 | return response()->json([ |
211 | "status" => "error", |
212 | "data" => [ |
213 | "message" => "Something went wrong. " . $th->getMessage() |
214 | ] |
215 | ], Response::HTTP_INTERNAL_SERVER_ERROR); |
216 | } |
217 | } |
218 | |
219 | public function updatePrompt(UpdatePromptRequest $request, SavedPrompt $prompt): JsonResponse |
220 | { |
221 | try { |
222 | $data = $request->validated(); |
223 | |
224 | $prompt = $this->savedPromptsService->update($prompt->id, $data); |
225 | |
226 | return response()->json([ |
227 | "status" => "updated", |
228 | "data" => [ |
229 | "prompt" => $prompt |
230 | ], |
231 | ], 200); |
232 | } catch (\Throwable $th) { |
233 | FlyMSGLogger::logError("updatePrompt", $th); |
234 | return response()->json([ |
235 | "status" => "error", |
236 | "data" => [ |
237 | "message" => "Something went wrong." . $th->getMessage() |
238 | ] |
239 | ], 500); |
240 | } |
241 | } |
242 | |
243 | |
244 | public function deletePrompt(Request $request, SavedPrompt $prompt): JsonResponse |
245 | { |
246 | try { |
247 | $request->validate([ |
248 | 'feature' => "required|string", |
249 | ]); |
250 | |
251 | if ($request->feature == 'flyengage') { |
252 | $prompt->delete(); |
253 | } else { |
254 | return response()->json([ |
255 | "status" => "error", |
256 | "data" => [ |
257 | "message" => "Feature not supported yet. Only flyengage is supported." |
258 | ] |
259 | ], 400); |
260 | } |
261 | |
262 | return response()->json([ |
263 | "status" => "deleted", |
264 | "data" => [ |
265 | "prompt" => null |
266 | ], |
267 | ], 200); |
268 | } catch (\Throwable $th) { |
269 | FlyMSGLogger::logError("deletePrompt", $th); |
270 | return response()->json([ |
271 | "status" => "error", |
272 | "data" => [ |
273 | "message" => "Something went wrong. " . $th->getMessage() |
274 | ] |
275 | ], 500); |
276 | } |
277 | } |
278 | |
279 | |
280 | /** |
281 | * Gets google refresh token |
282 | * @param Request $request |
283 | * @return JsonResponse |
284 | */ |
285 | public function getGoogleToken(Request $request): JsonResponse |
286 | { |
287 | $method = $request->method(); |
288 | $url = $request->url(); |
289 | $headers = $request->headers->all(); |
290 | $params = $request->all(); |
291 | |
292 | if ($method != 'GET') { |
293 | $headers = json_encode($request->headers->all()); |
294 | $params = json_encode($request->all()); |
295 | } |
296 | |
297 | Log::info('GOOGLE HTTP Request: ' . $method . ' ' . $url, [ |
298 | 'headers' => $headers, |
299 | 'params' => $params, |
300 | 'user' => $request->user() |
301 | ]); |
302 | |
303 | $geminiapi_token = GeminiAPI::refreshAccessToken(); |
304 | $translate_token = GoogleTranslate::refreshGoogleTranslateAccessToken(); |
305 | return response()->json([ |
306 | 'message' => 'Refreshed Google Token', |
307 | 'geminiapi_token' => $geminiapi_token, |
308 | 'translate_token' => $translate_token, |
309 | ]); |
310 | } |
311 | |
312 | /** |
313 | * Resets a users default prompt to vengresos' default. |
314 | * |
315 | * @param Request $request |
316 | * @return JsonResponse |
317 | */ |
318 | public function resetPrompt(Request $request, SavedPrompt $prompt): JsonResponse |
319 | { |
320 | try { |
321 | $data = $request->validate([ |
322 | 'name' => 'required|string', |
323 | 'feature' => 'required|string', |
324 | ]); |
325 | |
326 | // Determine the default prompt based on the feature |
327 | if ($data['feature'] == 'flyengage') { |
328 | $model = PromptModel::where('is_active', true)->latest()->first(); |
329 | $setting = PromptSetting::where('prompt_model_id', $model->id)->where('is_active', true)->where('feature', 'flyengage')->latest()->first(); |
330 | $promptBase = PromptType::where('prompt_setting_id', $setting->id)->where('is_active', true)->where('feature', 'flyengage')->where('name', $data['name'])->first(); |
331 | |
332 | $defaultPrompt = $promptBase->mission; |
333 | } elseif ($data['feature'] == 'flypost') { |
334 | $defaultPrompt = Constants::DEFAULT_FLYPOST_PROMPTS[$data['name']] ?? null; |
335 | } else { |
336 | throw new \Exception("Feature not supported yet. Only flyengage and flypost are supported."); |
337 | } |
338 | |
339 | // If the default prompt is not found, throw an error |
340 | if (!$defaultPrompt) { |
341 | throw new \Exception("No default prompt found for the specified feature and name."); |
342 | } |
343 | |
344 | // Update the saved prompt with the default |
345 | $prompt->update([ |
346 | 'prompt' => $defaultPrompt, |
347 | 'name' => $data['name'], |
348 | ]); |
349 | |
350 | return response()->json([ |
351 | "status" => "success", |
352 | "data" => [ |
353 | "prompt" => $prompt |
354 | ], |
355 | ], 200); |
356 | } catch (\Throwable $th) { |
357 | FlyMSGLogger::logError("resetPrompt", $th); |
358 | return response()->json([ |
359 | "status" => "error", |
360 | "data" => [ |
361 | "message" => "Something went wrong. " . $th->getMessage() |
362 | ] |
363 | ], 500); |
364 | } |
365 | } |
366 | |
367 | function getTypingSpeedConfigByFeature($feature, $user) |
368 | { |
369 | $parameters = Parameter::where('name', 'like', 'flywrite_typing%')->get()->keyBy('name'); |
370 | |
371 | $flypostTypingMode = $parameters['flywrite_typing_post_typing_mode']->value ?? 'letter'; |
372 | $flypostTypingSpeed = $parameters['flywrite_typing_post_typing_speed']->value ?? Constants::FLYPOST_FE_TYPING_SPEED_PER_MINUTE; |
373 | $flyengageTypingMode = $parameters['flywrite_typing_engage_typing_mode']->value ?? 'letter'; |
374 | $flyengageTypingSpeed = $parameters['flywrite_typing_engage_typing_speed']->value ?? Constants::FLYENGAGE_FE_TYPING_SPEED_PER_MINUTE; |
375 | |
376 | $userSetting = Setting::where('user_id', $user->id)->first(); |
377 | |
378 | return match ($feature) { |
379 | "flypost" => [ |
380 | "type" => $userSetting?->typing_style ?? $flypostTypingMode, |
381 | "words_per_minute" => (!empty($userSetting?->typing_style) && $userSetting?->typing_style == 'letter') ? ($userSetting->typing_speed ?? $flypostTypingSpeed) : $flypostTypingSpeed |
382 | ], |
383 | "flyengage" => [ |
384 | "type" => $userSetting?->typing_style ?? $flyengageTypingMode, |
385 | "words_per_minute" => (!empty($userSetting?->typing_style) && $userSetting?->typing_style == 'letter') ? ($userSetting->typing_speed ?? $flyengageTypingSpeed) : $flyengageTypingSpeed |
386 | ], |
387 | default => null |
388 | }; |
389 | } |
390 | } |