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