Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.29% covered (success)
94.29%
198 / 210
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
RolePlayConversationsController
94.29% covered (success)
94.29%
198 / 210
57.14% covered (warning)
57.14%
4 / 7
35.23
0.00% covered (danger)
0.00%
0 / 1
 start
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
1
 process
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 end
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
3.00
 recent
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 conversation
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 history
97.44% covered (success)
97.44%
38 / 39
0.00% covered (danger)
0.00%
0 / 1
8
 agents
100.00% covered (success)
100.00%
90 / 90
100.00% covered (success)
100.00%
1 / 1
13
1<?php
2
3namespace App\Http\Controllers\v2\RolePlay;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\Parameter;
7use App\Http\Models\RolePlayConversations;
8use App\Http\Models\RolePlayProjects;
9use App\Http\Models\UserAddOns;
10use App\Http\Requests\v2\RolePlay\GetRolePlayAgentsRequest;
11use App\Http\Requests\v2\RolePlay\RolePlaySessionEndRequest;
12use App\Http\Requests\v2\RolePlay\RolePlaySessionHistoryRequest;
13use App\Http\Requests\v2\RolePlay\RolePlaySessionProcessRequest;
14use App\Http\Requests\v2\RolePlay\RolePlaySessionStartRequest;
15use App\Jobs\ProcessRolePlaySessionAsyncJob;
16use App\Traits\SubscriptionTrait;
17use Illuminate\Http\JsonResponse;
18use Illuminate\Http\Request;
19use MongoDB\BSON\UTCDateTime;
20
21class RolePlayConversationsController extends Controller
22{
23    use SubscriptionTrait;
24
25    public function start(RolePlaySessionStartRequest $request): JsonResponse
26    {
27        $user = $request->user();
28        $project_id = $request->input('project_id');
29        $icp = $request->input('icp');
30        $prompt = $request->input('prompt');
31        $agent = $request->input('agent');
32
33        $conversation = RolePlayConversations::create([
34            'user_id' => $user->id,
35            'project_id' => $project_id,
36            'icp' => $icp,
37            'prompt' => $prompt,
38            'agent' => $agent,
39            'duration' => 0,
40            'status' => 'created',
41        ]);
42
43        $rolePlayAddOn = UserAddOns::where('user_id', $user->id)
44            ->where('product', 'RolePlay')
45            ->where('status', 'active')
46            ->orderBy('created_at', 'desc')
47            ->first();
48
49        $rolePlayAddOn->load('addOn');
50
51        $monthly_total_time_credits = $rolePlayAddOn->addOn->features['monthly_total_time_credits'] ?? 0;
52        $usedRolePlayTimeCredits = RolePlayConversations::where('user_id', $user->id)
53            ->where('status', 'done')
54            ->where('created_at', '>=', new UTCDateTime(strtotime(now()->startOfMonth()->toDateString()) * 1000))
55            ->sum('duration') ?? 0;
56
57        return response()->json([
58            'status' => 'success',
59            'data' => [
60                'session' => $conversation,
61                'seconds_remaining' => max(0, $monthly_total_time_credits - $usedRolePlayTimeCredits),
62            ],
63        ]);
64    }
65
66    public function process(RolePlaySessionProcessRequest $request, RolePlayConversations $conversation): JsonResponse
67    {
68        $project = RolePlayProjects::find($conversation->project_id);
69
70        if (empty($conversation->feedback) || true) {
71            ProcessRolePlaySessionAsyncJob::dispatchSync($conversation, $conversation->id);
72        } else {
73            $project->calculateProgression($conversation->feedback);
74        }
75
76        $conversation = $conversation->fresh();
77
78        $conversation->load('project');
79
80        return response()->json([
81            'status' => 'success',
82            'data' => $conversation,
83        ]);
84    }
85
86    public function end(RolePlaySessionEndRequest $request, RolePlayConversations $conversation): JsonResponse
87    {
88        $user = $request->user();
89
90        if ($conversation->user_id !== $user->id) {
91            return response()->json(['status' => 'error', 'message' => 'Unauthorized'], 403);
92        }
93
94        $vapiCallId = $request->input('vapi_call_id');
95
96        $conversation->status = 'processing';
97        if ($vapiCallId) {
98            $conversation->vapi_call_id = $vapiCallId;
99        }
100        $conversation->save();
101
102        ProcessRolePlaySessionAsyncJob::dispatch($conversation, $conversation->id, $vapiCallId);
103
104        return response()->json([
105            'status' => 'success',
106            'data' => $conversation,
107        ]);
108    }
109
110    public function recent(Request $request): JsonResponse
111    {
112        $user = $request->user();
113
114        $conversations = RolePlayConversations::where('user_id', $user->id)
115            ->where('status', 'done')
116            ->latest()
117            ->take(5)
118            ->get()
119            ->load('project')
120            ->map(function ($conversation) {
121                $conversation->icp = is_string($conversation->icp) ? json_decode($conversation->icp, true) : (array) $conversation->icp;
122                $conversation->agent = is_string($conversation->agent) ? json_decode($conversation->agent, true) : (array) $conversation->agent;
123
124                return $conversation;
125            });
126
127        return response()->json([
128            'status' => 'success',
129            'data' => $conversations,
130        ]);
131    }
132
133    public function conversation(Request $request, RolePlayConversations $conversation): JsonResponse
134    {
135        $user = $request->user();
136
137        if ($conversation->user_id !== $user->id) {
138            return response()->json(['status' => 'error', 'message' => 'Unauthorized'], 403);
139        }
140
141        $conversation->load('project');
142        $conversation->icp = is_string($conversation->icp) ? json_decode($conversation->icp, true) : (array) $conversation->icp;
143        $conversation->agent = is_string($conversation->agent) ? json_decode($conversation->agent, true) : (array) $conversation->agent;
144
145        return response()->json([
146            'status' => 'success',
147            'data' => $conversation,
148        ]);
149    }
150
151    public function history(RolePlaySessionHistoryRequest $request): JsonResponse
152    {
153        $user = $request->user();
154        $period = $request->input('period', 'all_times');
155
156        $startDate = null;
157        $endDate = null;
158
159        switch ($period) {
160            case 'this_week':
161                $startDate = now()->startOfWeek()->subMinute();
162                $endDate = now()->endOfWeek()->addMinute();
163                break;
164            case 'this_month':
165                $startDate = now()->startOfMonth()->subMinute();
166                $endDate = now()->endOfMonth()->addMinute();
167                break;
168        }
169
170        $query = RolePlayConversations::where('user_id', $user->id);
171
172        if ($startDate && $endDate) {
173            $query->whereBetween('created_at', [
174                new UTCDateTime(strtotime($startDate->toDateString()) * 1000),
175                new UTCDateTime(strtotime($endDate->toDateString()) * 1000),
176            ]);
177        } elseif ($endDate) {
178            $query->where('created_at', '<', new UTCDateTime(strtotime($endDate->toDateString()) * 1000));
179        }
180
181        $conversations = $query->select([
182            'id',
183            'project_id',
184            'score',
185            'status',
186            'duration',
187            'project',
188            'icp',
189            'agent',
190            'created_at',
191        ])->with('project')->latest()->get()->load('project')->map(function ($conversation) {
192            $conversation->icp = is_string($conversation->icp) ? json_decode($conversation->icp, true) : (array) $conversation->icp;
193            $conversation->agent = is_string($conversation->agent) ? json_decode($conversation->agent, true) : (array) $conversation->agent;
194
195            return $conversation;
196        });
197
198        return response()->json([
199            'status' => 'success',
200            'data' => $conversations,
201        ]);
202    }
203
204    public function agents(GetRolePlayAgentsRequest $request, RolePlayProjects $persona): JsonResponse
205    {
206        $maleNamesParam = Parameter::where('name', 'role_play_male_names')->first();
207        $maleImagesParam = Parameter::where('name', 'role_play_male_images')->first();
208        $maleVoicesParam = Parameter::where('name', 'role_play_male_voices')->first();
209        $femaleNamesParam = Parameter::where('name', 'role_play_female_names')->first();
210        $femaleImagesParam = Parameter::where('name', 'role_play_female_images')->first();
211        $femaleVoicesParam = Parameter::where('name', 'role_play_female_voices')->first();
212        $personalitiesParam = Parameter::where('name', 'role_play_personalities')->first();
213        $coldCallPromptParam = Parameter::where('name', 'role_play_cold_call_prompt')->first();
214        $discoveryCallPromptParam = Parameter::where('name', 'role_play_discovery_call_prompt')->first();
215
216        $maleNames = $maleNamesParam->value ?? [];
217        $maleImages = $maleImagesParam->value ?? [];
218        $maleVoices = $maleVoicesParam->value ?? [];
219        $femaleNames = $femaleNamesParam->value ?? [];
220        $femaleImages = $femaleImagesParam->value ?? [];
221        $femaleVoices = $femaleVoicesParam->value ?? [];
222        $personalities = $personalitiesParam->value ?? [];
223        $icps = $persona->customer_profiles ?? [];
224
225        $baseAgents = [];
226        shuffle($maleImages);
227        shuffle($femaleImages);
228        shuffle($personalities);
229
230        $maleImageCount = count($maleImages);
231        $femaleImageCount = count($femaleImages);
232        $personalityCount = count($personalities);
233        $personalityIndex = 0;
234
235        if ($maleImageCount > 0 && $personalityCount > 0 && ! empty($maleVoices)) {
236            foreach ($maleNames as $index => $name) {
237                $baseAgents[] = [
238                    'name' => $name,
239                    'gender' => 'male',
240                    'image' => $maleImages[$index % $maleImageCount],
241                    'voice' => $maleVoices[array_rand($maleVoices)],
242                    'personality' => $personalities[($personalityIndex++) % $personalityCount],
243                ];
244            }
245        }
246
247        if ($femaleImageCount > 0 && $personalityCount > 0 && ! empty($femaleVoices)) {
248            foreach ($femaleNames as $index => $name) {
249                $baseAgents[] = [
250                    'name' => $name,
251                    'gender' => 'female',
252                    'image' => $femaleImages[$index % $femaleImageCount],
253                    'voice' => $femaleVoices[array_rand($femaleVoices)],
254                    'personality' => $personalities[($personalityIndex++) % $personalityCount],
255                ];
256            }
257        }
258
259        shuffle($baseAgents);
260
261        $responseData = [];
262
263        $promptTemplate = ($persona->type === 'cold-call')
264            ? ($coldCallPromptParam->value ?? '')
265            : ($discoveryCallPromptParam->value ?? '');
266
267        foreach ($icps as $icp) {
268            $icpArray = is_array($icp) ? $icp : (array) $icp;
269
270            $person_description = "\n• Company Name: {$icpArray['company_name']}";
271            $person_description .= "\n• Company Size: {$icpArray['company_size']}";
272            $person_description .= "\n• Current Pain Points: {$icpArray['pain_points']}";
273            $person_description .= "\n• Decision-Making Authority: {$icpArray['decision_making']}";
274            $person_description .= "\n• Current Solution: {$icpArray['current_solution']}";
275            $person_description .= "\n• Budget: {$icpArray['budget']}";
276            $person_description .= "\n• Urgency Level: {$icpArray['urgency_level']}";
277            $person_description .= "\n• Openness to New Solutions: {$icpArray['openess_to_new_solutions']}";
278            $person_description .= "\n• Communication Style: {$icpArray['communication_style']}";
279            $person_description .= "\n• Personality: {$icpArray['personality']}\n";
280
281            $allObjections = array_map(function ($objection) {
282                $options = is_array($objection['options']) ? $objection['options'] : [];
283                $optionsString = implode("\n", array_map(fn ($opt) => '- '.$opt, $options));
284
285                return "{$objection['category']}:\n{$optionsString}";
286            }, $persona->objections ?? []);
287            $allObjectionsString = implode("\n\n", $allObjections);
288
289            $agentsWithPrompt = array_map(function ($agent) use ($persona, $person_description, $allObjectionsString, $promptTemplate) {
290                $placeholders = [
291                    '{name}',
292                    '{targetIndustry}',
293                    '{persona_prompt}',
294                    '{all_objections}',
295                ];
296
297                $replacements = [
298                    $agent['name'],
299                    $persona->industry,
300                    trim($person_description),
301                    $allObjectionsString,
302                ];
303
304                $agent['prompt'] = str_replace($placeholders, $replacements, $promptTemplate);
305
306                return $agent;
307            }, $baseAgents);
308
309            $responseData[] = [
310                'icp' => $icp,
311                'agents' => $agentsWithPrompt,
312            ];
313        }
314
315        return response()->json([
316            'status' => 'success',
317            'data' => $responseData,
318        ]);
319    }
320}