Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 102
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AiChatService
0.00% covered (danger)
0.00%
0 / 102
0.00% covered (danger)
0.00%
0 / 6
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 generateReply
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 buildSystemPrompt
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
6
 callGemini
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 callOpenAI
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 callClaude
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace App\Service;
4
5use App\Models\admin\Product;
6use App\Models\PaymentType;
7use App\Models\Setting;
8use App\Models\Shipping;
9use App\Models\SocialSetting;
10use Illuminate\Support\Facades\Http;
11use Illuminate\Support\Facades\Log;
12
13class AiChatService
14{
15    protected string $provider;
16
17    protected string $apiKey;
18
19    protected string $model;
20
21    protected string $systemPrompt;
22
23    public function __construct()
24    {
25        $this->provider = (string) SocialSetting::getSetting('ai_provider', 'gemini');
26        $this->apiKey = (string) SocialSetting::getSetting('ai_api_key', '');
27        $this->model = (string) SocialSetting::getSetting('ai_model', '');
28        $this->systemPrompt = (string) SocialSetting::getSetting('ai_system_prompt', '');
29    }
30
31    /**
32     * Generate AI reply based on conversation history
33     *
34     * @param  array  $history  [['role'=>'user','content'=>'...'], ...]
35     * @param  string  $userMessage  Latest user message
36     */
37    public function generateReply(array $history, string $userMessage): string
38    {
39        if (empty($this->apiKey)) {
40            return 'দুঃখিত, AI সেটিংস সঠিকভাবে কনফিগার করা হয়নি।';
41        }
42
43        // Build the full system prompt with dynamic business context
44        $fullSystemPrompt = $this->buildSystemPrompt();
45
46        // Append latest user message to history
47        $history[] = ['role' => 'user', 'content' => $userMessage];
48
49        try {
50            return match ($this->provider) {
51                'openai' => $this->callOpenAI($fullSystemPrompt, $history),
52                'claude' => $this->callClaude($fullSystemPrompt, $history),
53                default => $this->callGemini($fullSystemPrompt, $history),
54            };
55        } catch (\Throwable $e) {
56            Log::error('AI Chat Error: ' . $e->getMessage());
57
58            return 'দুঃখিত, এখন উত্তর দিতে পারছি না। একটু পরে আবার চেষ্টা করুন।';
59        }
60    }
61
62    /**
63     * Build dynamic system prompt with business data
64     */
65    protected function buildSystemPrompt(): string
66    {
67        $businessName = Setting::where('key', 'site_name')->value('value') ?? 'আমাদের শপ';
68        $businessPhone = Setting::where('key', 'phone')->value('value') ?? '';
69        $businessAddress = Setting::where('key', 'address')->value('value') ?? '';
70
71        // Get products
72        $products = Product::select('name', 'selling_price', 'product_code')
73            ->limit(50)
74            ->get()
75            ->map(fn ($p) => "{$p->name} (Code: {$p->product_code}) — মূল্য: {$p->selling_price} টাকা")
76            ->join("\n");
77
78        // Get shipping/delivery info
79        // BUG FIX: this used to say "Dhaka" with a 60/120 split — but the
80        // real checkout flow (CheckoutController::store()) has always charged
81        // 70/120 based on Chittagong, not Dhaka (see checkout.blade.php's
82        // "Inside/Outside Chittagong" radio buttons). The chatbot was quoting
83        // customers the wrong city and the wrong "inside" rate. Now matches
84        // the real checkout pricing exactly.
85        $shippings = "- চট্টগ্রামের ভেতরে: 70 টাকা\n- চট্টগ্রামের বাইরে (সারা বাংলাদেশ): 120 টাকা";
86
87        // Get payment types
88        // BUG FIX: this used to query the PaymentType table (used by the
89        // admin/POS side) and fall back to "Cash on Delivery, bKash" — but
90        // the actual website checkout has no payment-method selection at
91        // all, it's Cash on Delivery only. Telling chatbot customers "bKash"
92        // is available when it isn't would create a real order-time mismatch.
93        $paymentTypes = 'শুধুমাত্র Cash on Delivery (COD) — পণ্য হাতে পাওয়ার সময় টাকা পরিশোধ করতে হবে। অগ্রিম পেমেন্টের দরকার নেই।';
94
95        // User-defined system prompt (dynamic from settings)
96        $userPrompt = ! empty($this->systemPrompt)
97            ? "\n\n## বিশেষ নির্দেশনা (Admin দ্বারা সেট করা):\n{$this->systemPrompt}"
98            : '';
99
100        return <<<PROMPT
101তুমি {$businessName}-এর একজন বুদ্ধিমান AI Customer Service Assistant।
102তুমি বাংলায় কথা বলবে এবং সবসময় বিনয়ী, আন্তরিক ও helpful হবে।
103তুমি একজন real মানুষের মতো কথা বলবে — রোবটিক ভাষা ব্যবহার করবে না।
104
105## ব্যবসার তথ্য:
106- নাম: {$businessName}
107- ফোন: {$businessPhone}
108- ঠিকানা: {$businessAddress}
109
110## আমাদের পণ্য তালিকা:
111{$products}
112
113## ডেলিভারি চার্জ:
114{$shippings}
115
116## পেমেন্ট পদ্ধতি:
117{$paymentTypes}
118
119## তোমার কাজ:
1201. Customer-এর প্রশ্নের উত্তর দাও (product info, price, stock, delivery সম্পর্কে)
1212. Customer order দিতে চাইলে নাম, ঠিকানা, ফোন নম্বর ও পণ্যের নাম জানো
1223. সব তথ্য পেলে "✅ আপনার অর্ডার নেওয়া হয়েছে" বলো এবং summary দাও
1234. Order confirm করতে এই format-এ লেখো:
124   ORDER_CONFIRM:{"name":"...","phone":"...","address":"...","product":"...","quantity":1}
1255. Delivery সম্পর্কে জানতে চাইলে delivery charge বলো
1266. কখনো database-এ না থাকা পণ্যের কথা বলবে না{$userPrompt}
127PROMPT;
128    }
129
130    /**
131     * Call Google Gemini API
132     */
133    protected function callGemini(string $systemPrompt, array $history): string
134    {
135        $model = ! empty($this->model) ? $this->model : 'gemini-1.5-flash';
136        $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$this->apiKey}";
137
138        // Convert history to Gemini format
139        $contents = [];
140        // Add system prompt as first user message
141        $contents[] = [
142            'role' => 'user',
143            'parts' => [['text' => $systemPrompt . "\n\nএখন customer-এর সাথে কথা শুরু করো।"]],
144        ];
145        $contents[] = [
146            'role' => 'model',
147            'parts' => [['text' => 'অবশ্যই! আমি সাহায্য করতে প্রস্তুত।']],
148        ];
149
150        foreach ($history as $msg) {
151            $contents[] = [
152                'role' => $msg['role'] === 'assistant' ? 'model' : 'user',
153                'parts' => [['text' => $msg['content']]],
154            ];
155        }
156
157        $response = Http::timeout(30)->post($url, [
158            'contents' => $contents,
159            'generationConfig' => [
160                'temperature' => 0.7,
161                'maxOutputTokens' => 500,
162            ],
163        ]);
164
165        if ($response->successful()) {
166            return $response->json('candidates.0.content.parts.0.text', 'দুঃখিত, উত্তর পাওয়া যায়নি।');
167        }
168
169        Log::error('Gemini API Error: ' . $response->body());
170
171        return 'দুঃখিত, AI সংযোগে সমস্যা হয়েছে।';
172    }
173
174    /**
175     * Call OpenAI (ChatGPT) API
176     */
177    protected function callOpenAI(string $systemPrompt, array $history): string
178    {
179        $model = ! empty($this->model) ? $this->model : 'gpt-4o-mini';
180        $messages = [['role' => 'system', 'content' => $systemPrompt]];
181
182        foreach ($history as $msg) {
183            $messages[] = ['role' => $msg['role'], 'content' => $msg['content']];
184        }
185
186        $response = Http::timeout(30)
187            ->withToken($this->apiKey)
188            ->post('https://api.openai.com/v1/chat/completions', [
189                'model' => $model,
190                'messages' => $messages,
191                'max_tokens' => 500,
192                'temperature' => 0.7,
193            ]);
194
195        if ($response->successful()) {
196            return $response->json('choices.0.message.content', 'দুঃখিত, উত্তর পাওয়া যায়নি।');
197        }
198
199        Log::error('OpenAI API Error: ' . $response->body());
200
201        return 'দুঃখিত, AI সংযোগে সমস্যা হয়েছে।';
202    }
203
204    /**
205     * Call Anthropic Claude API
206     */
207    protected function callClaude(string $systemPrompt, array $history): string
208    {
209        $model = ! empty($this->model) ? $this->model : 'claude-3-5-haiku-20241022';
210        $messages = [];
211
212        foreach ($history as $msg) {
213            $messages[] = ['role' => $msg['role'], 'content' => $msg['content']];
214        }
215
216        $response = Http::timeout(30)
217            ->withHeaders([
218                'x-api-key' => $this->apiKey,
219                'anthropic-version' => '2023-06-01',
220                'Content-Type' => 'application/json',
221            ])
222            ->post('https://api.anthropic.com/v1/messages', [
223                'model' => $model,
224                'system' => $systemPrompt,
225                'messages' => $messages,
226                'max_tokens' => 500,
227            ]);
228
229        if ($response->successful()) {
230            return $response->json('content.0.text', 'দুঃখিত, উত্তর পাওয়া যায়নি।');
231        }
232
233        Log::error('Claude API Error: ' . $response->body());
234
235        return 'দুঃখিত, AI সংযোগে সমস্যা হয়েছে।';
236    }
237}