Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.33% covered (success)
93.33%
364 / 390
60.00% covered (warning)
60.00%
9 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
RolePlayPersonasController
93.33% covered (success)
93.33%
364 / 390
60.00% covered (warning)
60.00%
9 / 15
55.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
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
97.14% covered (success)
97.14%
34 / 35
0.00% covered (danger)
0.00%
0 / 1
2
 regenerate_icps
97.73% covered (success)
97.73%
43 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 companyProjects
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
5.20
 cloneCompanyProject
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
3
 autoPopulate
88.68% covered (warning)
88.68%
47 / 53
0.00% covered (danger)
0.00%
0 / 1
7.07
 chatCompletionAutoPopulate
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultAutoPopulatePromptTemplate
n/a
0 / 0
n/a
0 / 0
1
 getDefaultAutoPopulateGroundingPromptTemplate
n/a
0 / 0
n/a
0 / 0
1
 generateAiPersonalities
91.18% covered (success)
91.18%
93 / 102
0.00% covered (danger)
0.00%
0 / 1
17.20
 chatCompletion
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
1
 parseGemini15Response
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\Http\Controllers\v2\RolePlay;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\Admin\CompanyGroup;
7use App\Http\Models\AIPrompts;
8use App\Http\Models\CompanyRolePlayProject;
9use App\Http\Models\Parameter;
10use App\Http\Models\RolePlayConfig;
11use App\Http\Models\RolePlayProjects;
12use App\Http\Requests\v2\RolePlay\AutoPopulateRolePlayProjectRequest;
13use App\Http\Requests\v2\RolePlay\DestroyRolePlayProjectRequest;
14use App\Http\Requests\v2\RolePlay\GenerateICPRolePlayProjectRequest;
15use App\Http\Requests\v2\RolePlay\GetRolePlayProjectRequest;
16use App\Http\Requests\v2\RolePlay\IndexRolePlayPersonasRequest;
17use App\Http\Requests\v2\RolePlay\RegenerateICPRolePlayProjectRequest;
18use App\Http\Requests\v2\RolePlay\StoreRolePlayProjectRequest;
19use App\Http\Requests\v2\RolePlay\UpdateRolePlayProjectRequest;
20use App\Http\Resources\v2\CompanyRolePlayProjectResource;
21use App\Http\Services\NodeJsAIBridgeService;
22use App\Http\Services\WebScraperService;
23use App\Traits\SubscriptionTrait;
24use Carbon\Carbon;
25use Illuminate\Http\JsonResponse;
26use Illuminate\Http\Request;
27
28class RolePlayPersonasController extends Controller
29{
30    use SubscriptionTrait;
31
32    public function __construct(
33        private readonly NodeJsAIBridgeService $bridge,
34        private readonly WebScraperService $scraper
35    ) {}
36
37    public function index(IndexRolePlayPersonasRequest $request): JsonResponse
38    {
39        $user = $request->user();
40        $validated = $request->validated();
41
42        $projects = RolePlayProjects::where('user_id', $user->id)
43            ->when(isset($validated['type']), function ($query) use ($validated) {
44                return $query->where('type', $validated['type']);
45            })
46            ->when(isset($validated['lastPracticed']), function ($query) use ($validated) {
47                return $query->whereHas('conversations', function ($q) use ($validated) {
48                    $q->where('created_at', '<=', Carbon::now()->subDays($validated['lastPracticed'])->startOfDay());
49                });
50            })
51            ->orderBy('created_at', 'desc')
52            ->get();
53
54        $projects->transform(function ($project) {
55            $lastConversation = $project->conversations()->latest()->first();
56            $project->last_practiced_at = $lastConversation ? $lastConversation->created_at : null;
57
58            return $project;
59        });
60
61        return response()->json([
62            'status' => 'success',
63            'data' => $projects,
64        ]);
65    }
66
67    public function get(GetRolePlayProjectRequest $request, RolePlayProjects $persona): JsonResponse
68    {
69        $persona->load('conversations');
70        $persona->load('user');
71
72        return response()->json([
73            'status' => 'success',
74            'data' => $persona,
75        ]);
76    }
77
78    public function store(StoreRolePlayProjectRequest $request): JsonResponse
79    {
80        $user = $request->user();
81        $validated = $request->validated();
82
83        $data = array_merge($validated, ['user_id' => $user->id]);
84        $data['training_personalities'] = $this->generateAiPersonalities($data);
85
86        $project = RolePlayProjects::create($data);
87
88        return response()->json([
89            'status' => 'success',
90            'data' => $project,
91        ], 201);
92    }
93
94    public function update(UpdateRolePlayProjectRequest $request, RolePlayProjects $persona): JsonResponse
95    {
96        $validated = $request->validated();
97
98        $maxIcpIndex = 0;
99        foreach ($persona->training_personalities ?? [] as $aiPersona) {
100            $icpArray = is_array($aiPersona) ? $aiPersona : (array) $aiPersona;
101
102            foreach ($icpArray['agents'] ?? [] as $aiAgent) {
103                $aiAgentArray = is_array($aiAgent) ? $aiAgent : (array) $aiAgent;
104                if (isset($aiAgentArray['id']) && is_int($aiAgentArray['id']) && $aiAgentArray['id'] > $maxIcpIndex) {
105                    $maxIcpIndex = $aiAgentArray['id'];
106                }
107            }
108        }
109
110        $validated['training_personalities'] = $this->generateAiPersonalities($validated, $maxIcpIndex);
111
112        $persona->update($validated);
113
114        return response()->json([
115            'status' => 'success',
116            'data' => $persona->fresh(),
117        ]);
118    }
119
120    public function destroy(DestroyRolePlayProjectRequest $request, RolePlayProjects $persona): JsonResponse
121    {
122        $persona->delete();
123
124        return response()->json([
125            'status' => 'success',
126        ]);
127    }
128
129    public function generate_icps(GenerateICPRolePlayProjectRequest $request): JsonResponse
130    {
131        $user = $request->user();
132        $validated = $request->validated();
133        $type = $validated['type'];
134        $industry = $validated['industry'];
135        $product_description = $validated['product_description'];
136        $difficulty_level = $validated['difficulty_level'];
137        $key_features = $validated['key_features'] ?? [];
138        // Try ai_prompts first, fallback to parameters
139        $promptRecord = AIPrompts::where('product', 'roleplay_generate_icp')
140            ->where('status', 'active')
141            ->first();
142
143        if ($promptRecord) {
144            $promptTemplate = $promptRecord->context;
145        } else {
146            $promptTemplate = Parameter::where('name', 'role_play_generate_icp_prompt')->first()?->value ?? '';
147        }
148
149        $placeholders = [
150            '{type}',
151            '{productDescription}',
152            '{key_features}',
153            '{targetIndustry}',
154            '{difficulty_level}',
155        ];
156
157        // $key_features = explode(',', $key_features);
158        $key_features = array_map(fn ($feature) => '- '.trim($feature), $key_features);
159        $key_features = implode("\n", $key_features);
160
161        $replacements = [
162            $type,
163            $product_description,
164            $key_features,
165            $industry,
166            $difficulty_level,
167        ];
168
169        $prompt = str_replace($placeholders, $replacements, $promptTemplate);
170
171        $result = $this->chatCompletion($prompt, $user);
172
173        return response()->json([
174            'status' => 'success',
175            'data' => $result,
176        ]);
177    }
178
179    public function regenerate_icps(RegenerateICPRolePlayProjectRequest $request): JsonResponse
180    {
181        $user = $request->user();
182        $validated = $request->validated();
183        $type = $validated['type'];
184        $industry = $validated['industry'];
185        $company_name = $validated['company_name'];
186        $company_size = $validated['company_size'];
187        $budget = $validated['budget'];
188        $product_description = $validated['product_description'];
189        $difficulty_level = $validated['difficulty_level'];
190        $key_features = $validated['key_features'] ?? [];
191        // Try ai_prompts first, fallback to parameters
192        $promptRecord = AIPrompts::where('product', 'roleplay_regenerate_icp')
193            ->where('status', 'active')
194            ->first();
195
196        if ($promptRecord) {
197            $promptTemplate = $promptRecord->context;
198        } else {
199            $promptTemplate = Parameter::where('name', 'role_play_regenerate_icp_prompt')->first()?->value ?? '';
200        }
201
202        $placeholders = [
203            '{type}',
204            '{productDescription}',
205            '{key_features}',
206            '{targetIndustry}',
207            '{company_name}',
208            '{company_size}',
209            '{budget}',
210            '{difficulty_level}',
211        ];
212
213        // $key_features = explode(',', $key_features);
214        $key_features = array_map(fn ($feature) => '- '.trim($feature), $key_features);
215        $key_features = implode("\n", $key_features);
216
217        $replacements = [
218            $type,
219            $product_description,
220            $key_features,
221            $industry,
222            $company_name,
223            $company_size,
224            $budget,
225            $difficulty_level,
226        ];
227
228        $prompt = str_replace($placeholders, $replacements, $promptTemplate);
229
230        $result = $this->chatCompletion($prompt, $user);
231
232        return response()->json([
233            'status' => 'success',
234            'data' => $result,
235        ]);
236    }
237
238    /**
239     * List company roleplay projects visible to the current user.
240     *
241     * Returns active company projects that are either assigned to
242     * the user's group (or parent group) or have no group restrictions.
243     *
244     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse
245     *
246     * @response 200 {"result": {"data": [...]}}
247     */
248    public function companyProjects(Request $request)
249    {
250        $user = $request->user();
251        $companyId = $user->company_id;
252
253        if (! $companyId) {
254            return response()->json([
255                'status' => 'success',
256                'data' => [],
257            ]);
258        }
259
260        // Collect the user's group IDs (direct group + parent group)
261        $userGroupIds = [];
262        if ($user->company_group_id) {
263            $userGroupIds[] = (string) $user->company_group_id;
264
265            $group = CompanyGroup::find($user->company_group_id);
266            if ($group && $group->parent_id) {
267                $userGroupIds[] = (string) $group->parent_id;
268            }
269        }
270
271        $projects = CompanyRolePlayProject::forCompany($companyId)
272            ->active()
273            ->forGroups($userGroupIds)
274            ->with('creator')
275            ->orderBy('created_at', 'desc')
276            ->get();
277
278        return CompanyRolePlayProjectResource::collection($projects);
279    }
280
281    /**
282     * Clone a company roleplay project to the user's own projects.
283     *
284     * Creates a new RolePlayProjects record with data copied from the
285     * company project. Only works if the company project has
286     * allow_user_customization enabled.
287     *
288     * @param  string  $id  The company project ID to clone
289     *
290     * @response 201 {"status": "success", "data": {...}}
291     * @response 403 {"status": "error", "message": "This project does not allow user customization"}
292     * @response 404 {"status": "error", "message": "Company project not found"}
293     */
294    public function cloneCompanyProject(Request $request, string $id): JsonResponse
295    {
296        $user = $request->user();
297        $companyId = $user->company_id;
298
299        $companyProject = CompanyRolePlayProject::forCompany($companyId)
300            ->active()
301            ->find($id);
302
303        if (! $companyProject) {
304            return response()->json([
305                'status' => 'error',
306                'message' => 'Company project not found',
307            ], 404);
308        }
309
310        if (! $companyProject->allow_user_customization) {
311            return response()->json([
312                'status' => 'error',
313                'message' => 'This project does not allow user customization',
314            ], 403);
315        }
316
317        $clonedProject = RolePlayProjects::create([
318            'name' => $companyProject->name,
319            'user_id' => $user->id,
320            'type' => $companyProject->type,
321            'description' => $companyProject->description,
322            'difficulty_level' => $companyProject->difficulty_level,
323            'key_features' => $companyProject->key_features ?? [],
324            'industry' => $companyProject->industry,
325            'target_job_titles' => $companyProject->target_job_titles ?? [],
326            'customer_profiles' => $companyProject->customer_profiles ?? [],
327            'training_personalities' => $companyProject->training_personalities ?? [],
328            'scorecard_config' => $companyProject->scorecard_config ?? [],
329            'objections' => $companyProject->objections ?? [],
330            'company_project_id' => $companyProject->id,
331            'is_clone' => true,
332            'source' => 'company',
333        ]);
334
335        return response()->json([
336            'status' => 'success',
337            'data' => $clonedProject,
338        ], 201);
339    }
340
341    /**
342     * Auto-populate roleplay project data from a website URL.
343     *
344     * Scrapes the provided website to extract company/product information,
345     * then uses AI to generate a complete roleplay project structure including
346     * name, product details, customer profiles, and objections.
347     *
348     * If the website cannot be scraped (e.g. anti-crawler protection), falls
349     * back to Gemini with Google Search grounding to research the URL.
350     *
351     *
352     * @response 200 {
353     *   "result": {
354     *     "status": "success",
355     *     "data": {
356     *       "name": "Enterprise SaaS Cold Call",
357     *       "product_name": "Example Platform",
358     *       "product_description": "AI-powered analytics platform...",
359     *       "target_audience": "VP of Sales at mid-market B2B companies...",
360     *       "call_type": "cold-call",
361     *       "difficulty": 3,
362     *       "training_personalities": ["assertive", "analytical"],
363     *       "customer_profiles": [
364     *         {
365     *           "name": "Sarah Chen",
366     *           "job_title": "VP of Sales",
367     *           "company_name": "TechCorp",
368     *           "company_size": "201-500",
369     *           "industry": "Technology",
370     *           "personality": "analytical",
371     *           "budget_authority": "Decision Maker",
372     *           "pain_points": ["Low conversion rates", "Manual reporting"],
373     *           "goals": ["Increase pipeline velocity", "Better forecasting"],
374     *           "background": "10+ years in B2B sales leadership..."
375     *         }
376     *       ],
377     *       "objections": [
378     *         {
379     *           "category": "Price",
380     *           "options": ["Too expensive compared to current solution", "No budget allocated this quarter"]
381     *         }
382     *       ]
383     *     }
384     *   }
385     * }
386     */
387    public function autoPopulate(AutoPopulateRolePlayProjectRequest $request): JsonResponse
388    {
389        $user = $request->user();
390        $validated = $request->validated();
391
392        $websiteUrl = $validated['website_url'];
393        $callType = $validated['call_type'] ?? null;
394        $difficulty = $validated['difficulty'] ?? 3;
395
396        // Step 1: Try to scrape website content
397        $websiteContent = $this->scraper->scrape($websiteUrl);
398
399        // Step 2: Build the AI prompt (with content or grounding-only fallback)
400        $promptRecord = AIPrompts::where('product', 'roleplay_auto_populate')
401            ->where('status', 'active')
402            ->first();
403
404        $hasContent = ! empty(trim($websiteContent));
405
406        if ($hasContent) {
407            $promptTemplate = $promptRecord
408                ? $promptRecord->context
409                : (Parameter::where('name', 'role_play_auto_populate_prompt')->first()?->value ?? '');
410
411            if (empty($promptTemplate)) {
412                $promptTemplate = $this->getDefaultAutoPopulatePromptTemplate();
413            }
414
415            $prompt = str_replace(
416                ['{website_url}', '{website_content}', '{call_type}', '{difficulty}'],
417                [
418                    $websiteUrl,
419                    $websiteContent,
420                    $callType ?? 'infer the most appropriate call type (cold-call or discovery-call) from the website content',
421                    (string) $difficulty,
422                ],
423                $promptTemplate
424            );
425        } else {
426            $groundingRecord = AIPrompts::where('product', 'roleplay_auto_populate_grounding')
427                ->where('status', 'active')
428                ->first();
429
430            $groundingTemplate = $groundingRecord
431                ? $groundingRecord->context
432                : (Parameter::where('name', 'role_play_auto_populate_grounding_prompt')->first()?->value ?? '');
433
434            if (empty($groundingTemplate)) {
435                $groundingTemplate = $this->getDefaultAutoPopulateGroundingPromptTemplate();
436            }
437
438            $prompt = str_replace(
439                ['{website_url}', '{call_type}', '{difficulty}'],
440                [
441                    $websiteUrl,
442                    $callType ?? 'infer the most appropriate call type (cold-call or discovery-call) based on the company',
443                    (string) $difficulty,
444                ],
445                $groundingTemplate
446            );
447        }
448
449        // Step 3: Call AI with Google Search grounding enabled
450        $result = $this->chatCompletionAutoPopulate($prompt, $user);
451
452        if ($result === null) {
453            return response()->json([
454                'status' => 'error',
455                'message' => 'Failed to generate persona data. Please try again.',
456            ], 500);
457        }
458
459        return response()->json([
460            'status' => 'success',
461            'data' => $result,
462        ]);
463    }
464
465    /**
466     * Call AI with Google Search grounding enabled for auto-populate.
467     *
468     * @param  string  $prompt  The prompt to send
469     * @param  mixed  $user  The authenticated user
470     * @return mixed Parsed JSON response or null on failure
471     */
472    private function chatCompletionAutoPopulate(string $prompt, mixed $user = null): mixed
473    {
474        $text = $this->bridge->generate(
475            [
476                'provider' => 'vertex',
477                'model' => 'gemini-2.5-flash',
478                'prompt' => $prompt,
479                'config' => [
480                    'maxOutputTokens' => 4000,
481                    'temperature' => 0.8,
482                    'topP' => 0.95,
483                    'thinkingBudget' => 0,
484                    'enableGoogleSearch' => true,
485                ],
486            ],
487            [
488                'feature' => 'persona_auto_populate',
489                'user_id' => $user?->id,
490                'company_id' => $user?->company_id ?? null,
491            ]
492        );
493
494        return $this->parseGemini15Response($text);
495    }
496
497    /**
498     * Default prompt template when website content was successfully scraped.
499     */
500    private function getDefaultAutoPopulatePromptTemplate(): string
501    {
502        return <<<'PROMPT'
503You are an expert sales training content creator. Analyze the following website content and generate a complete sales roleplay training project.
504
505Website URL: {website_url}
506
507Website Content:
508---
509{website_content}
510---
511
512Call Type: {call_type}
513Difficulty Level (1-5): {difficulty}
514
515Based on the website content, generate a JSON object with the following structure:
516{
517  "name": "A descriptive project name for this roleplay scenario",
518  "product_name": "The product/service name from the website",
519  "product_description": "A concise description of the product/service (2-3 sentences)",
520  "target_audience": "Description of the ideal target audience for this product",
521  "call_type": "cold-call or discovery-call",
522  "difficulty": <difficulty level as integer 1-5>,
523  "training_personalities": ["personality1", "personality2"],
524  "customer_profiles": [
525    {
526      "name": "A realistic full name",
527      "job_title": "Relevant job title",
528      "company_name": "A realistic company name",
529      "company_size": "One of: 1-50, 51-200, 201-500, 501-1000, 1001-5000, 5001+",
530      "industry": "The industry sector",
531      "personality": "A personality trait (e.g. analytical, assertive, friendly, skeptical)",
532      "budget_authority": "One of: Decision Maker, Influencer, Gatekeeper, Champion",
533      "pain_points": ["pain point 1", "pain point 2", "pain point 3"],
534      "goals": ["goal 1", "goal 2"],
535      "background": "A brief professional background (2-3 sentences)"
536    }
537  ],
538  "objections": [
539    {
540      "category": "An objection category (e.g. Price, Timing, Competition, Need, Authority)",
541      "options": ["specific objection 1", "specific objection 2"]
542    }
543  ]
544}
545
546Requirements:
547- Generate 2-3 customer profiles with diverse personalities and seniority levels
548- Generate 3-5 objection categories with 2-3 options each
549- Make pain points and objections specific to the product/industry
550- Ensure the difficulty level matches: 1=very easy prospect, 5=very resistant prospect
551- All content should be realistic and based on the actual website content
552- Return ONLY the JSON object, no additional text
553PROMPT;
554    }
555
556    /**
557     * Default prompt template when scraping failed â€” relies on Gemini Google Search grounding.
558     */
559    private function getDefaultAutoPopulateGroundingPromptTemplate(): string
560    {
561        return <<<'PROMPT'
562You are an expert sales training content creator. Research the following website and company using your knowledge and available search tools, then generate a complete sales roleplay training project.
563
564Website URL: {website_url}
565
566Please research this company/product thoroughly. Look up what they do, their target market, their key products and services, and their value proposition.
567
568Call Type: {call_type}
569Difficulty Level (1-5): {difficulty}
570
571Based on your research, generate a JSON object with the following structure:
572{
573  "name": "A descriptive project name for this roleplay scenario",
574  "product_name": "The product/service name from the website",
575  "product_description": "A concise description of the product/service (2-3 sentences)",
576  "target_audience": "Description of the ideal target audience for this product",
577  "call_type": "cold-call or discovery-call",
578  "difficulty": <difficulty level as integer 1-5>,
579  "training_personalities": ["personality1", "personality2"],
580  "customer_profiles": [
581    {
582      "name": "A realistic full name",
583      "job_title": "Relevant job title",
584      "company_name": "A realistic company name",
585      "company_size": "One of: 1-50, 51-200, 201-500, 501-1000, 1001-5000, 5001+",
586      "industry": "The industry sector",
587      "personality": "A personality trait (e.g. analytical, assertive, friendly, skeptical)",
588      "budget_authority": "One of: Decision Maker, Influencer, Gatekeeper, Champion",
589      "pain_points": ["pain point 1", "pain point 2", "pain point 3"],
590      "goals": ["goal 1", "goal 2"],
591      "background": "A brief professional background (2-3 sentences)"
592    }
593  ],
594  "objections": [
595    {
596      "category": "An objection category (e.g. Price, Timing, Competition, Need, Authority)",
597      "options": ["specific objection 1", "specific objection 2"]
598    }
599  ]
600}
601
602Requirements:
603- Generate 2-3 customer profiles with diverse personalities and seniority levels
604- Generate 3-5 objection categories with 2-3 options each
605- Make pain points and objections specific to the product/industry
606- Ensure the difficulty level matches: 1=very easy prospect, 5=very resistant prospect
607- All content should be realistic and based on the company's actual offerings
608- Return ONLY the JSON object, no additional text
609PROMPT;
610    }
611
612    private function generateAiPersonalities($persona, int $currentMaxIndex = 0): array
613    {
614        // Try RolePlayConfig first, fallback to Parameters
615        $config = RolePlayConfig::getGlobal();
616
617        if ($config) {
618            $maleNames = $config->names['male'] ?? [];
619            $maleImages = $config->images['male'] ?? [];
620            $maleVoices = $config->voices['male'] ?? [];
621            $femaleNames = $config->names['female'] ?? [];
622            $femaleImages = $config->images['female'] ?? [];
623            $femaleVoices = $config->voices['female'] ?? [];
624            $personalities = $config->personalities ?? [];
625        } else {
626            $maleNames = Parameter::where('name', 'role_play_male_names')->first()?->value ?? [];
627            $maleImages = Parameter::where('name', 'role_play_male_images')->first()?->value ?? [];
628            $maleVoices = Parameter::where('name', 'role_play_male_voices')->first()?->value ?? [];
629            $femaleNames = Parameter::where('name', 'role_play_female_names')->first()?->value ?? [];
630            $femaleImages = Parameter::where('name', 'role_play_female_images')->first()?->value ?? [];
631            $femaleVoices = Parameter::where('name', 'role_play_female_voices')->first()?->value ?? [];
632            $personalities = Parameter::where('name', 'role_play_personalities')->first()?->value ?? [];
633        }
634
635        // Try ai_prompts first for call prompts, fallback to parameters
636        $coldCallPromptRecord = AIPrompts::where('product', 'roleplay_cold_call')
637            ->where('status', 'active')
638            ->first();
639        $discoveryCallPromptRecord = AIPrompts::where('product', 'roleplay_discovery_call')
640            ->where('status', 'active')
641            ->first();
642
643        $coldCallPromptValue = $coldCallPromptRecord
644            ? $coldCallPromptRecord->context
645            : (Parameter::where('name', 'role_play_cold_call_prompt')->first()?->value ?? '');
646        $discoveryCallPromptValue = $discoveryCallPromptRecord
647            ? $discoveryCallPromptRecord->context
648            : (Parameter::where('name', 'role_play_discovery_call_prompt')->first()?->value ?? '');
649
650        $icps = $persona['customer_profiles'] ?? [];
651
652        $baseAgents = [];
653        shuffle($maleImages);
654        shuffle($femaleImages);
655        shuffle($personalities);
656
657        $maleImageCount = count($maleImages);
658        $femaleImageCount = count($femaleImages);
659        $personalityCount = count($personalities);
660        $personalityIndex = 0;
661
662        if ($maleImageCount > 0 && $personalityCount > 0 && ! empty($maleVoices)) {
663            foreach ($maleNames as $index => $name) {
664                $baseAgents[] = [
665                    'name' => $name,
666                    'gender' => 'male',
667                    'image' => $maleImages[$index % $maleImageCount],
668                    'voice' => $maleVoices[array_rand($maleVoices)],
669                    'personality' => $personalities[($personalityIndex++) % $personalityCount],
670                ];
671            }
672        }
673
674        if ($femaleImageCount > 0 && $personalityCount > 0 && ! empty($femaleVoices)) {
675            foreach ($femaleNames as $index => $name) {
676                $baseAgents[] = [
677                    'name' => $name,
678                    'gender' => 'female',
679                    'image' => $femaleImages[$index % $femaleImageCount],
680                    'voice' => $femaleVoices[array_rand($femaleVoices)],
681                    'personality' => $personalities[($personalityIndex++) % $personalityCount],
682                ];
683            }
684        }
685
686        shuffle($baseAgents);
687
688        $responseData = [];
689
690        $promptTemplate = ($persona['type'] === 'cold-call')
691            ? $coldCallPromptValue
692            : $discoveryCallPromptValue;
693
694        $indexForId = $currentMaxIndex + 1;
695
696        foreach ($icps as $icp_index => $icp) {
697            foreach ($baseAgents as $agent_index => $baseAgentId) {
698                $baseAgents[$agent_index]['id'] = $indexForId++;
699            }
700
701            $icpArray = is_array($icp) ? $icp : (array) $icp;
702
703            $person_description = "\n• Company Name: {$icpArray['company_name']}";
704            $person_description .= "\n• Company Size: {$icpArray['company_size']}";
705            $person_description .= "\n• Current Pain Points: {$icpArray['pain_points']}";
706            $person_description .= "\n• Decision-Making Authority: {$icpArray['decision_making']}";
707            $person_description .= "\n• Current Solution: {$icpArray['current_solution']}";
708            $person_description .= "\n• Budget: {$icpArray['budget']}";
709            $person_description .= "\n• Urgency Level: {$icpArray['urgency_level']}";
710            $person_description .= "\n• Openness to New Solutions: {$icpArray['openess_to_new_solutions']}";
711            $person_description .= "\n• Communication Style: {$icpArray['communication_style']}";
712            $person_description .= "\n• Personality: {$icpArray['personality']}\n";
713
714            $allObjections = array_map(function ($objection) {
715                $options = is_array($objection['options']) ? $objection['options'] : [];
716                $optionsString = implode("\n", array_map(fn ($opt) => '- '.$opt, $options));
717
718                return "{$objection['category']}:\n{$optionsString}";
719            }, $persona['objections'] ?? []);
720            $allObjectionsString = implode("\n\n", $allObjections);
721
722            $agentsWithPrompt = array_map(function ($agent) use ($persona, $person_description, $allObjectionsString, $promptTemplate) {
723                $placeholders = [
724                    '{name}',
725                    '{targetIndustry}',
726                    '{persona_prompt}',
727                    '{all_objections}',
728                ];
729
730                $replacements = [
731                    $agent['name'],
732                    $persona['industry'],
733                    trim($person_description),
734                    $allObjectionsString,
735                ];
736
737                $agent['prompt'] = str_replace($placeholders, $replacements, $promptTemplate);
738
739                return $agent;
740            }, $baseAgents);
741
742            $responseData[] = [
743                'icp' => $icp,
744                'agents' => $agentsWithPrompt,
745            ];
746        }
747
748        return $responseData;
749    }
750
751    private function chatCompletion(string $prompt, mixed $user = null): mixed
752    {
753        $text = $this->bridge->generate(
754            [
755                'provider' => 'vertex',
756                'model' => 'gemini-2.5-flash',
757                'prompt' => $prompt,
758                'config' => [
759                    'maxOutputTokens' => 2500,
760                    'temperature' => 1.0,
761                    'topP' => 1.0,
762                    'thinkingBudget' => 0,
763                    'enableGoogleSearch' => false,
764                ],
765            ],
766            [
767                'feature' => 'persona_generate',
768                'user_id' => $user?->id,
769                'company_id' => $user?->company_id ?? null,
770            ]
771        );
772
773        return $this->parseGemini15Response($text);
774    }
775
776    private function parseGemini15Response(string $text): mixed
777    {
778        $completion = str_replace('```json', '', $text);
779        $completion = str_replace('```', '', $completion);
780        $completion = trim($completion);
781
782        return json_decode($completion, true);
783    }
784}