Skip to content

Commit 584d706

Browse files
committed
feat: implement AI-driven lead creation with enhanced data mapping and person entity handling
1 parent 761d619 commit 584d706

File tree

3 files changed

+129
-77
lines changed

3 files changed

+129
-77
lines changed

packages/Webkul/Admin/src/Helpers/Lead.php

Lines changed: 55 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ public static function extractDataFromPdf($pdfPath)
1414
{
1515
try {
1616
$parser = new Parser;
17-
$pdf = $parser->parseFile($pdfPath);
18-
$text = trim($pdf->getText());
1917

20-
if (empty($text)) {
18+
$pdfText = trim($parser->parseFile($pdfPath)->getText());
19+
20+
if (empty($pdfText)) {
2121
throw new Exception('PDF content is empty or could not be extracted.');
2222
}
2323

24-
return self::sendLLMRequest($text);
24+
return self::sendLLMRequest($pdfText);
2525
} catch (Exception $e) {
2626
return ['error' => $e->getMessage()];
2727
}
@@ -30,12 +30,13 @@ public static function extractDataFromPdf($pdfPath)
3030
/**
3131
* Send a request to the LLM API.
3232
*/
33-
private static function sendLLMRequest($prompt)
33+
private static function sendLLMRequest($prompt)
3434
{
3535
$model = core()->getConfigData('general.magic_ai.settings.model');
3636
$apiKey = core()->getConfigData('general.magic_ai.settings.api_key');
37+
$apiDomain = core()->getConfigData('general.magic_ai.settings.api_domain');
3738

38-
if (empty($apiKey) || empty($model)) {
39+
if (! $apiKey || ! $model) {
3940
return ['error' => 'Missing API key or model configuration.'];
4041
}
4142

@@ -44,84 +45,68 @@ private static function sendLLMRequest($prompt)
4445
}
4546

4647
$apiUrlMap = [
47-
'gpt-4o' => 'https://api.openai.com/v1/chat/completions',
48-
'gpt-4o-mini' => 'https://api.openai.com/v1/chat/completions',
49-
'llama3.2:latest' => core()->getConfigData('general.magic_ai.settings.api_domain') . '/v1/chat/completions',
50-
'deepseek-r1:8b' => core()->getConfigData('general.magic_ai.settings.api_domain') . '/v1/chat/completions',
51-
'gemini-2.0-flash' => 'https://api.gemini-ai.com/v1/chat/completions',
48+
'gpt-4o' => 'https://api.openai.com/v1/chat/completions',
49+
'gpt-4o-mini' => 'https://api.openai.com/v1/chat/completions',
50+
'llama3.2:latest' => "$apiDomain/v1/chat/completions",
51+
'deepseek-r1:8b' => "$apiDomain/v1/chat/completions",
5252
];
5353

5454
$apiUrl = $apiUrlMap[$model] ?? 'https://api.groq.com/openai/v1/chat/completions';
5555

56-
$data = self::prepareRequestData($model, $prompt);
57-
58-
return self::makeCurlRequest($apiUrl, $model, json_encode($data), true);
56+
return self::makeCurlRequest($apiUrl, $model, self::prepareRequestData($model, $prompt));
5957
}
6058

59+
/**
60+
* Send Request to Gemini AI.
61+
*/
6162
private static function sendGeminiRequest($prompt, $model)
6263
{
63-
$url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=' . core()->getConfigData('general.magic_ai.settings.api_key');
64-
64+
$apiKey = core()->getConfigData('general.magic_ai.settings.api_key');
65+
66+
if (empty($apiKey)) {
67+
return ['error' => 'Missing Google API key.'];
68+
}
69+
70+
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
71+
6572
$data = self::prepareRequestData($model, $prompt);
66-
67-
return self::makeCurlRequest($url, $model, json_encode($data), true);
68-
}
6973

70-
private static function makeCurlRequest($url, $model, $prompt, $isJson = false)
71-
{
72-
$apiKey = core()->getConfigData('general.magic_ai.settings.api_key');
74+
$data['stream'] = false;
7375

74-
$data = $isJson ? json_decode($prompt, true) : [
75-
'model' => $model,
76-
'messages' => [
77-
[
78-
'role' => 'system',
79-
'content' => 'You are an AI assistant. You have to extract the data from the PDF file.
80-
Example Output:
81-
{
82-
"status": 1,
83-
"title": "Untitled Lead",
84-
"person": {
85-
"name": "Unknown",
86-
"email": null,
87-
"phone": null,
88-
"organization_id": null
89-
},
90-
"lead_pipeline_stage_id": null,
91-
"value": 0,
92-
"source": "AI Extracted"
93-
}
94-
Note: Only return the output, Do not return or add any comments.',
95-
],
96-
['role' => 'user', 'content' => 'PDF:\n' . $prompt],
97-
],
98-
];
76+
return self::makeCurlRequest($url, $model, $data);
77+
}
9978

79+
/**
80+
* Request data to AI using Curl API.
81+
*/
82+
private static function makeCurlRequest($url, $model, array $data)
83+
{
10084
try {
101-
$ch = curl_init();
102-
103-
curl_setopt($ch, CURLOPT_URL, $url);
104-
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
105-
curl_setopt($ch, CURLOPT_POST, true);
106-
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
107-
curl_setopt($ch, CURLOPT_HTTPHEADER, [
108-
'Content-Type: application/json',
109-
'Authorization: Bearer ' . $apiKey,
85+
$ch = curl_init($url);
86+
87+
curl_setopt_array($ch, [
88+
CURLOPT_RETURNTRANSFER => true,
89+
CURLOPT_POST => true,
90+
CURLOPT_POSTFIELDS => json_encode($data),
91+
CURLOPT_HTTPHEADER => [
92+
'Content-Type: application/json',
93+
'Authorization: Bearer '.core()->getConfigData('general.magic_ai.settings.api_key'),
94+
],
11095
]);
11196

11297
$response = curl_exec($ch);
11398
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
11499

115100
if (curl_errno($ch)) {
116-
throw new Exception('cURL Error: ' . curl_error($ch));
101+
throw new Exception('cURL Error: '.curl_error($ch));
117102
}
118103

119104
curl_close($ch);
120105

121106
$decodedResponse = json_decode($response, true);
122107

123108
if ($httpCode !== 200 || isset($decodedResponse['error'])) {
124-
throw new Exception('LLM API Error: ' . ($decodedResponse['error']['message'] ?? 'Unknown error'));
109+
throw new Exception('LLM API Error: '.($decodedResponse['error']['message'] ?? 'Unknown error'));
125110
}
126111

127112
return $decodedResponse;
@@ -130,6 +115,9 @@ private static function makeCurlRequest($url, $model, $prompt, $isJson = false)
130115
}
131116
}
132117

118+
/**
119+
* Prepare request data for AI.
120+
*/
133121
private static function prepareRequestData($model, $prompt)
134122
{
135123
return [
@@ -144,17 +132,22 @@ private static function prepareRequestData($model, $prompt)
144132
"title": "Untitled Lead",
145133
"person": {
146134
"name": "Unknown",
147-
"email": null,
148-
"phone": null,
149-
"organization_id": null
135+
"emails": {
136+
"value": null,
137+
"label": null
138+
},
139+
"contact_numbers": {
140+
"value": null,
141+
"label": null
142+
}
150143
},
151144
"lead_pipeline_stage_id": null,
152145
"value": 0,
153146
"source": "AI Extracted"
154147
}
155148
Note: Only return the output, Do not return or add any comments.',
156149
],
157-
['role' => 'user', 'content' => 'PDF:\n' . $prompt],
150+
['role' => 'user', 'content' => "PDF:\n$prompt"],
158151
],
159152
];
160153
}

packages/Webkul/Admin/src/Http/Controllers/Lead/LeadController.php

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public function __construct(
4545
protected StageRepository $stageRepository,
4646
protected LeadRepository $leadRepository,
4747
protected ProductRepository $productRepository,
48+
protected PersonRepository $personRepository
4849
) {
4950
request()->request->add(['entity_type' => 'leads']);
5051
}
@@ -627,6 +628,9 @@ private function getKanbanColumns(): array
627628
];
628629
}
629630

631+
/**
632+
* Create Lead with specified AI.
633+
*/
630634
public function createByAI(LeadForm $request)
631635
{
632636
if ($pdfFile = $request->file('file')) {
@@ -641,17 +645,13 @@ public function createByAI(LeadForm $request)
641645
], 400);
642646
}
643647

644-
dd($extractedData);
645-
646648
$leadData = $this->mapAIDataToLead($extractedData);
647649

648650
$validatedData = app(LeadForm::class)->validated();
649651

650652
$finalData = array_merge($validatedData, $leadData);
651653

652-
dd($finalData);
653-
654-
return $this->store(new LeadForm($finalData));
654+
return self::leadCreate($finalData);
655655
}
656656

657657
return response()->json([
@@ -660,20 +660,77 @@ public function createByAI(LeadForm $request)
660660
], 400);
661661
}
662662

663+
/**
664+
* Mapped the receive Extracted AI data.
665+
*/
663666
private function mapAIDataToLead($aiData)
664667
{
668+
$content = $aiData['choices'][0]['message']['content'] ?? '';
669+
670+
$content = preg_replace('/<[^>]+>/', '', $content);
671+
672+
$jsonParts = preg_split('/(?=\{\s*"status"\s*:)/', $content);
673+
674+
$finalData = json_decode($jsonParts[1]);
675+
665676
return [
666-
'status' => 1,
667-
'title' => $aiData['lead_title'] ?? 'Untitled Lead',
668-
'person' => [
669-
'name' => $aiData['contact_name'] ?? 'Unknown',
670-
'email' => $aiData['contact_email'] ?? null,
671-
'phone' => $aiData['contact_phone'] ?? null,
672-
'organization_id' => $aiData['organization_id'] ?? null,
677+
'status' => 1,
678+
'title' => $finalData->title ?? 'N/A',
679+
'description' => $finalData->description ?? null,
680+
'lead_source_id' => 1,
681+
'lead_type_id' => 1,
682+
'lead_value' => $finalData->lead_value ?? 0,
683+
'person' => [
684+
'name' => $finalData->person->name ?? 'Unknown',
685+
'emails' => [
686+
0 => [
687+
'value' => $finalData->person->emails->value ?? null,
688+
'label' => $finalData->person->emails->label ?? 'work',
689+
],
690+
],
691+
'contact_numbers' => [
692+
0 => [
693+
'value' => $finalData->person->contact_numbers->value ?? null,
694+
'label' => $finalData->person->contact_numbers->label ?? 'work',
695+
],
696+
],
697+
'entity_type' => 'persons',
673698
],
674-
'lead_pipeline_stage_id' => $aiData['pipeline_stage_id'] ?? null,
675-
'value' => $aiData['lead_value'] ?? 0,
676-
'source' => $aiData['source'] ?? 'AI Extracted',
699+
'entity_type' => 'leads',
677700
];
678701
}
702+
703+
/**
704+
* Create lead independent entity.
705+
*/
706+
private function leadCreate($data)
707+
{
708+
$person = $this->personRepository->create($data['person']);
709+
710+
$pipeline = $this->pipelineRepository->getDefaultPipeline();
711+
712+
$stage = $pipeline->stages()->first();
713+
714+
$data = array_merge($data, [
715+
'lead_pipeline_id' => $pipeline->id,
716+
'lead_pipeline_stage_id' => $stage->id,
717+
'expected_close_date' => Carbon::now()->addDays(7),
718+
'person' => [
719+
'id' => $person->id,
720+
'organization_id' => $data['person']['organization_id'] ?? null,
721+
]
722+
]);
723+
724+
if (in_array($stage->code, ['won', 'lost'])) {
725+
$data['closed_at'] = Carbon::now();
726+
}
727+
728+
$lead = $this->leadRepository->create($data);
729+
730+
Event::dispatch('lead.create.after', $lead);
731+
732+
return response()->json([
733+
'message' => 'Lead successfully created.',
734+
], 200);
735+
}
679736
}

packages/Webkul/Admin/src/Resources/views/leads/index/upload.blade.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ class="primary-button justify-center"
100100
this.$emitter.emit('add-flash', { type: 'success', message: response.data.message });
101101
102102
this.$refs.userUpdateAndCreateModal.close();
103+
104+
window.location.reload();
103105
})
104106
.catch (error => {
105107
this.isLoading = false;

0 commit comments

Comments
 (0)