Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.07% covered (warning)
82.07%
206 / 251
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
RolePlayPersonasController
82.07% covered (warning)
82.07%
206 / 251
70.00% covered (warning)
70.00%
7 / 10
42.06
0.00% covered (danger)
0.00%
0 / 1
 index
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 store
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 update
64.29% covered (warning)
64.29%
9 / 14
0.00% covered (danger)
0.00%
0 / 1
10.92
 destroy
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 generate_icps
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
1
 regenerate_icps
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
2
 generateAiPersonalities
98.89% covered (success)
98.89%
89 / 90
0.00% covered (danger)
0.00%
0 / 1
14
 chatCompletion
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 parseGemini15Response
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace App\Http\Controllers\v2\RolePlay;
4
5use App\Traits\SubscriptionTrait;
6use Illuminate\Http\JsonResponse;
7use App\Http\Controllers\Controller;
8use App\Http\Models\Parameter;
9use App\Http\Models\RolePlayProjects;
10use App\Http\Requests\v2\RolePlay\DestroyRolePlayProjectRequest;
11use App\Http\Requests\v2\RolePlay\GenerateICPRolePlayProjectRequest;
12use App\Http\Requests\v2\RolePlay\GetRolePlayProjectRequest;
13use App\Http\Requests\v2\RolePlay\IndexRolePlayPersonasRequest;
14use App\Http\Requests\v2\RolePlay\RegenerateICPRolePlayProjectRequest;
15use App\Http\Requests\v2\RolePlay\StoreRolePlayProjectRequest;
16use App\Http\Requests\v2\RolePlay\UpdateRolePlayProjectRequest;
17use App\Services\FlyMsgAI\GeminiAPI;
18use Carbon\Carbon;
19use Illuminate\Support\Facades\Log;
20
21class RolePlayPersonasController extends Controller
22{
23    use SubscriptionTrait;
24
25    public function index(IndexRolePlayPersonasRequest $request): JsonResponse
26    {
27        $user = $request->user();
28        $validated = $request->validated();
29
30        $projects = RolePlayProjects::where('user_id', $user->id)
31            ->when(isset($validated['type']), function ($query) use ($validated) {
32                return $query->where('type', $validated['type']);
33            })
34            ->when(isset($validated['lastPracticed']), function ($query) use ($validated) {
35                return $query->whereHas('conversations', function ($q) use ($validated) {
36                    $q->where('created_at', '<=', Carbon::now()->subDays($validated['lastPracticed'])->startOfDay());
37                });
38            })
39            ->orderBy('created_at', 'desc')
40            ->get();
41
42        $projects->transform(function ($project) {
43            $lastConversation = $project->conversations()->latest()->first();
44            $project->last_practiced_at = $lastConversation ? $lastConversation->created_at : null;
45            return $project;
46        });
47
48        return response()->json([
49            'status' => 'success',
50            'data' => $projects,
51        ]);
52    }
53
54    public function get(GetRolePlayProjectRequest $request, RolePlayProjects $persona): JsonResponse
55    {
56        $persona->load('conversations');
57        $persona->load('user');
58
59        return response()->json([
60            'status' => 'success',
61            'data' => $persona,
62        ]);
63    }
64
65    public function store(StoreRolePlayProjectRequest $request): JsonResponse
66    {
67        $user = $request->user();
68        $validated = $request->validated();
69
70        $data = array_merge($validated, ['user_id' => $user->id]);
71        $data['training_personalities'] = $this->generateAiPersonalities($data);
72
73        $project = RolePlayProjects::create($data);
74
75        return response()->json([
76            'status' => 'success',
77            'data' => $project,
78        ], 201);
79    }
80
81    public function update(UpdateRolePlayProjectRequest $request, RolePlayProjects $persona): JsonResponse
82    {
83        $validated = $request->validated();
84
85        $maxIcpIndex = 0;
86        foreach ($persona->training_personalities ?? [] as $aiPersona) {
87            $icpArray = is_array($aiPersona) ? $aiPersona : (array) $aiPersona;
88
89            foreach ($icpArray['agents'] ?? [] as $aiAgent) {
90                $aiAgentArray = is_array($aiAgent) ? $aiAgent : (array) $aiAgent;
91                if (isset($aiAgentArray['id']) && is_int($aiAgentArray['id']) && $aiAgentArray['id'] > $maxIcpIndex) {
92                    $maxIcpIndex = $aiAgentArray['id'];
93                }
94            }
95        }
96
97        $validated['training_personalities'] = $this->generateAiPersonalities($validated, $maxIcpIndex);
98
99        $persona->update($validated);
100
101        return response()->json([
102            'status' => 'success',
103            'data' => $persona->fresh(),
104        ]);
105    }
106
107    public function destroy(DestroyRolePlayProjectRequest $request, RolePlayProjects $persona): JsonResponse
108    {
109        $persona->delete();
110
111        return response()->json([
112            'status' => 'success',
113        ]);
114    }
115
116    public function generate_icps(GenerateICPRolePlayProjectRequest $request): JsonResponse
117    {
118        $validated = $request->validated();
119        $type = $validated['type'];
120        $industry = $validated['industry'];
121        $product_description = $validated['product_description'];
122        $difficulty_level = $validated['difficulty_level'];
123        $key_features = $validated['key_features'] ?? [];
124        $generateICPPromptParam = Parameter::where('name', 'role_play_generate_icp_prompt')->first();
125        $promptTemplate = $generateICPPromptParam->value ?? '';
126        $placeholders = [
127            '{type}',
128            '{productDescription}',
129            '{key_features}',
130            '{targetIndustry}',
131            '{difficulty_level}'
132        ];
133
134        // $key_features = explode(',', $key_features);
135        $key_features = array_map(fn($feature) => "- " . trim($feature), $key_features);
136        $key_features = implode("\n", $key_features);
137
138        $replacements = [
139            $type,
140            $product_description,
141            $key_features,
142            $industry,
143            $difficulty_level
144        ];
145
146        $prompt = str_replace($placeholders, $replacements, $promptTemplate);
147
148        $result = $this->chatCompletion($prompt);
149
150        return response()->json([
151            'status' => 'success',
152            'data' => $result,
153        ]);
154    }
155
156    public function regenerate_icps(RegenerateICPRolePlayProjectRequest $request): JsonResponse
157    {
158        $validated = $request->validated();
159        $type = $validated['type'];
160        $industry = $validated['industry'];
161        $company_name = $validated['company_name'];
162        $company_size = $validated['company_size'];
163        $budget = $validated['budget'];
164        $product_description = $validated['product_description'];
165        $difficulty_level = $validated['difficulty_level'];
166        $key_features = $validated['key_features'] ?? [];
167        $generateICPPromptParam = Parameter::where('name', 'role_play_regenerate_icp_prompt')->first();
168        $promptTemplate = $generateICPPromptParam->value ?? '';
169        $placeholders = [
170            '{type}',
171            '{productDescription}',
172            '{key_features}',
173            '{targetIndustry}',
174            '{company_name}',
175            '{company_size}',
176            '{budget}',
177            '{difficulty_level}'
178        ];
179
180        // $key_features = explode(',', $key_features);
181        $key_features = array_map(fn($feature) => "- " . trim($feature), $key_features);
182        $key_features = implode("\n", $key_features);
183
184        $replacements = [
185            $type,
186            $product_description,
187            $key_features,
188            $industry,
189            $company_name,
190            $company_size,
191            $budget,
192            $difficulty_level
193        ];
194
195        $prompt = str_replace($placeholders, $replacements, $promptTemplate);
196
197        $result = $this->chatCompletion($prompt);
198
199        return response()->json([
200            'status' => 'success',
201            'data' => $result,
202        ]);
203    }
204
205    private function generateAiPersonalities($persona, int $currentMaxIndex = 0): array
206    {
207        $maleNamesParam = Parameter::where('name', 'role_play_male_names')->first();
208        $maleImagesParam = Parameter::where('name', 'role_play_male_images')->first();
209        $maleVoicesParam = Parameter::where('name', 'role_play_male_voices')->first();
210        $femaleNamesParam = Parameter::where('name', 'role_play_female_names')->first();
211        $femaleImagesParam = Parameter::where('name', 'role_play_female_images')->first();
212        $femaleVoicesParam = Parameter::where('name', 'role_play_female_voices')->first();
213        $personalitiesParam = Parameter::where('name', 'role_play_personalities')->first();
214        $coldCallPromptParam = Parameter::where('name', 'role_play_cold_call_prompt')->first();
215        $discoveryCallPromptParam = Parameter::where('name', 'role_play_discovery_call_prompt')->first();
216
217        $maleNames = $maleNamesParam->value ?? [];
218        $maleImages = $maleImagesParam->value ?? [];
219        $maleVoices = $maleVoicesParam->value ?? [];
220        $femaleNames = $femaleNamesParam->value ?? [];
221        $femaleImages = $femaleImagesParam->value ?? [];
222        $femaleVoices = $femaleVoicesParam->value ?? [];
223        $personalities = $personalitiesParam->value ?? [];
224        $icps = $persona['customer_profiles'] ?? [];
225
226        $baseAgents = [];
227        shuffle($maleImages);
228        shuffle($femaleImages);
229        shuffle($personalities);
230
231        $maleImageCount = count($maleImages);
232        $femaleImageCount = count($femaleImages);
233        $personalityCount = count($personalities);
234        $personalityIndex = 0;
235
236        if ($maleImageCount > 0 && $personalityCount > 0 && !empty($maleVoices)) {
237            foreach ($maleNames as $index => $name) {
238                $baseAgents[] = [
239                    'name' => $name,
240                    'gender' => 'male',
241                    'image' => $maleImages[$index % $maleImageCount],
242                    'voice' => $maleVoices[array_rand($maleVoices)],
243                    'personality' => $personalities[($personalityIndex++) % $personalityCount],
244                ];
245            }
246        }
247
248        if ($femaleImageCount > 0 && $personalityCount > 0 && !empty($femaleVoices)) {
249            foreach ($femaleNames as $index => $name) {
250                $baseAgents[] = [
251                    'name' => $name,
252                    'gender' => 'female',
253                    'image' => $femaleImages[$index % $femaleImageCount],
254                    'voice' => $femaleVoices[array_rand($femaleVoices)],
255                    'personality' => $personalities[($personalityIndex++) % $personalityCount],
256                ];
257            }
258        }
259
260        shuffle($baseAgents);
261
262        $responseData = [];
263
264        $promptTemplate = ($persona['type'] === 'cold-call')
265            ? ($coldCallPromptParam->value ?? '')
266            : ($discoveryCallPromptParam->value ?? '');
267
268        $indexForId = $currentMaxIndex + 1;
269
270        foreach ($icps as $icp_index => $icp) {
271            foreach ($baseAgents as $agent_index => $baseAgentId) {
272                $baseAgents[$agent_index]['id'] = $indexForId++;
273            }
274
275            $icpArray = is_array($icp) ? $icp : (array) $icp;
276
277            $person_description = "\n• Company Name: {$icpArray['company_name']}";
278            $person_description .= "\n• Company Size: {$icpArray['company_size']}";
279            $person_description .= "\n• Current Pain Points: {$icpArray['pain_points']}";
280            $person_description .= "\n• Decision-Making Authority: {$icpArray['decision_making']}";
281            $person_description .= "\n• Current Solution: {$icpArray['current_solution']}";
282            $person_description .= "\n• Budget: {$icpArray['budget']}";
283            $person_description .= "\n• Urgency Level: {$icpArray['urgency_level']}";
284            $person_description .= "\n• Openess to New Solutions: {$icpArray['openess_to_new_solutions']}";
285            $person_description .= "\n• Communication Style: {$icpArray['communication_style']}";
286            $person_description .= "\n• Personality: {$icpArray['personality']}\n";
287
288            $allObjections = array_map(function ($objection) {
289                $options = is_array($objection['options']) ? $objection['options'] : [];
290                $optionsString = implode("\n", array_map(fn($opt) => "- " . $opt, $options));
291                return "{$objection['category']}:\n{$optionsString}";
292            }, $persona['objections'] ?? []);
293            $allObjectionsString = implode("\n\n", $allObjections);
294
295            $agentsWithPrompt = array_map(function ($agent) use ($persona, $person_description, $allObjectionsString, $promptTemplate) {
296                $placeholders = [
297                    '{name}',
298                    '{targetIndustry}',
299                    '{persona_prompt}',
300                    '{all_objections}',
301                ];
302
303                $replacements = [
304                    $agent['name'],
305                    $persona['industry'],
306                    trim($person_description),
307                    $allObjectionsString,
308                ];
309
310                $agent['prompt'] = str_replace($placeholders, $replacements, $promptTemplate);
311                return $agent;
312            }, $baseAgents);
313
314            $responseData[] = [
315                'icp' => $icp,
316                'agents' => $agentsWithPrompt,
317            ];
318        }
319
320        return $responseData;
321    }
322
323    private function chatCompletion(string $prompt)
324    {
325        $model = "gemini-2.5-flash:streamGenerateContent";
326        $access_token = GeminiAPI::getAIAccessToken();
327
328        $generationConfig = [
329            "maxOutputTokens" => 2500,
330            "temperature" => 1,
331            "topP" => 1,
332            "thinkingConfig" => [
333                "thinkingBudget" => 0,
334            ],
335            // "topK" => 1
336        ];
337
338        $geminiAPI = new GeminiAPI($access_token);
339
340        $data = [
341            "contents" => [
342                "role" => "user",
343                "parts" => [
344                    [
345                        "text" => $prompt,
346                    ]
347                ]
348            ],
349            "generationConfig" => $generationConfig,
350        ];
351
352        $response = $geminiAPI->postCompletions($data, $model);
353        $responseData = json_decode($response->getBody()->getContents(), true);
354
355        $generatedResponse = $this->parseGemini15Response($responseData);
356
357        return $generatedResponse;
358    }
359
360    private function parseGemini15Response($response)
361    {
362        $extractedText = '';
363
364        foreach ($response as $message) {
365            foreach ($message['candidates'] as $candidate) {
366                if (isset($candidate['content'])) {
367                    foreach ($candidate['content']['parts'] as $part) {
368                        $extractedText .= $part['text'];
369                    }
370                }
371            }
372        }
373
374        $completion = str_replace('```json', '', $extractedText);
375        $completion = str_replace('```', '', $completion);
376        $completion = trim($completion);
377
378        return json_decode($completion, true);
379    }
380}