Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.06% |
198 / 204 |
|
77.78% |
7 / 9 |
CRAP | |
0.00% |
0 / 1 |
RolePlayPersonasController | |
97.06% |
198 / 204 |
|
77.78% |
7 / 9 |
34 | |
0.00% |
0 / 1 |
index | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
2 | |||
get | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
store | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
update | |
64.29% |
9 / 14 |
|
0.00% |
0 / 1 |
10.92 | |||
destroy | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
generate_icps | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
1 | |||
generateAiPersonalities | |
98.89% |
89 / 90 |
|
0.00% |
0 / 1 |
14 | |||
chatCompletion | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
1 | |||
parseGemini15Response | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | |
3 | namespace App\Http\Controllers\v2\RolePlay; |
4 | |
5 | use App\Traits\SubscriptionTrait; |
6 | use Illuminate\Http\JsonResponse; |
7 | use App\Http\Controllers\Controller; |
8 | use App\Http\Models\Parameter; |
9 | use App\Http\Models\RolePlayProjects; |
10 | use App\Http\Requests\v2\RolePlay\DestroyRolePlayProjectRequest; |
11 | use App\Http\Requests\v2\RolePlay\GenerateICPRolePlayProjectRequest; |
12 | use App\Http\Requests\v2\RolePlay\GetRolePlayProjectRequest; |
13 | use App\Http\Requests\v2\RolePlay\IndexRolePlayPersonasRequest; |
14 | use App\Http\Requests\v2\RolePlay\StoreRolePlayProjectRequest; |
15 | use App\Http\Requests\v2\RolePlay\UpdateRolePlayProjectRequest; |
16 | use App\Services\FlyMsgAI\GeminiAPI; |
17 | use Carbon\Carbon; |
18 | use Illuminate\Support\Facades\Log; |
19 | |
20 | class 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 | } |