$chat->company_id, 'ai_chat_id' => $chat->id, 'role' => 'user', 'content' => $userMessage, ]); $company = Company::withoutGlobalScopes()->findOrFail($chat->company_id); $aiCfg = (array) ($company->settings['ai'] ?? []); $provider = $chat->provider ?: ($aiCfg['default_provider'] ?? 'claude'); $apiKey = $aiCfg["{$provider}_key"] ?? null; $start = microtime(true); if (! $apiKey) { $reply = '⚠️ API key pentru ' . strtoupper($provider) . ' nu e configurat. Mergi la Setări → AI și adaugă cheia.'; $meta = ['error' => 'no_api_key']; } else { try { [$reply, $meta] = match ($provider) { 'claude' => $this->callClaude($apiKey, $chat, $userMessage, $company), 'gpt' => $this->callOpenAI($apiKey, $chat, $userMessage, $company), 'gemini' => $this->callGemini($apiKey, $chat, $userMessage, $company), default => ['Provider necunoscut: ' . $provider, []], }; } catch (\Throwable $e) { $reply = '❌ Eroare API: ' . $e->getMessage(); $meta = ['error' => $e->getMessage()]; } } $meta['latency_ms'] = (int) ((microtime(true) - $start) * 1000); $meta['provider'] = $provider; $msg = AiMessage::create([ 'company_id' => $chat->company_id, 'ai_chat_id' => $chat->id, 'role' => 'assistant', 'content' => $reply, 'meta' => $meta, ]); // Update chat title from first user message. if ($chat->messages()->where('role', 'user')->count() === 1) { $chat->update(['title' => mb_substr($userMessage, 0, 60)]); } $chat->touch(); return $msg; } protected function buildSystemPrompt(Company $company): string { $clientsCount = Client::count(); $vehiclesCount = Vehicle::count(); $newLeads = Lead::where('status', 'new')->count(); $openWO = WorkOrder::whereNotIn('status', ['done', 'cancelled'])->count(); $debtTotal = (float) WorkOrder::where('pay_status', '!=', 'paid') ->whereNotIn('status', ['cancelled']) ->get()->sum(fn ($w) => max(0, (float) $w->total - (float) $w->payments()->sum('amount'))); return <<name}" din {$company->city}. Răspunzi în română (sau în limba folosită de utilizator). Fii concis, profesional, util. Statistici curente: - {$clientsCount} clienți, {$vehiclesCount} mașini - {$newLeads} cereri noi neprocesate - {$openWO} fișe de lucru active - Total datorii clienți: {$debtTotal} MDL Poți ajuta cu: sugestii diagnostic auto, calcul costuri, sumar performanță, recomandări piese, redactare mesaje pentru clienți, planificare. NU inventezi date care nu apar în context — dacă nu știi, recunoaște. TXT; } /** Convert chat history to provider-specific message format. */ protected function historyMessages(AiChat $chat, int $limit = 20): array { return $chat->messages() ->latest('created_at') ->limit($limit) ->get() ->reverse() ->values() ->map(fn (AiMessage $m) => ['role' => $m->role, 'content' => $m->content]) ->all(); } protected function callClaude(string $key, AiChat $chat, string $msg, Company $company): array { $messages = $this->historyMessages($chat); // Anthropic requires alternating user/assistant; system is separate. $messages = array_values(array_filter($messages, fn ($m) => in_array($m['role'], ['user', 'assistant'], true))); $r = Http::withHeaders([ 'x-api-key' => $key, 'anthropic-version' => '2023-06-01', 'content-type' => 'application/json', ]) ->timeout(60) ->post('https://api.anthropic.com/v1/messages', [ 'model' => 'claude-sonnet-4-5', 'max_tokens' => 1024, 'system' => $this->buildSystemPrompt($company), 'messages' => $messages, ]); if (! $r->successful()) { return ['❌ ' . ($r->json('error.message') ?? 'Anthropic API error ' . $r->status()), ['status' => $r->status()]]; } $body = $r->json(); $text = collect($body['content'] ?? []) ->where('type', 'text') ->pluck('text') ->implode("\n"); return [$text ?: '(răspuns gol)', [ 'model' => $body['model'] ?? null, 'tokens_in' => $body['usage']['input_tokens'] ?? null, 'tokens_out' => $body['usage']['output_tokens'] ?? null, ]]; } protected function callOpenAI(string $key, AiChat $chat, string $msg, Company $company): array { $messages = array_merge( [['role' => 'system', 'content' => $this->buildSystemPrompt($company)]], $this->historyMessages($chat), ); $r = Http::withHeaders(['Authorization' => 'Bearer ' . $key, 'content-type' => 'application/json']) ->timeout(60) ->post('https://api.openai.com/v1/chat/completions', [ 'model' => 'gpt-4o-mini', 'messages' => $messages, 'max_tokens' => 1024, ]); if (! $r->successful()) { return ['❌ ' . ($r->json('error.message') ?? 'OpenAI error ' . $r->status()), ['status' => $r->status()]]; } $body = $r->json(); return [ $body['choices'][0]['message']['content'] ?? '(răspuns gol)', [ 'model' => $body['model'] ?? null, 'tokens_in' => $body['usage']['prompt_tokens'] ?? null, 'tokens_out' => $body['usage']['completion_tokens'] ?? null, ], ]; } protected function callGemini(string $key, AiChat $chat, string $msg, Company $company): array { $contents = []; foreach ($this->historyMessages($chat) as $m) { $contents[] = [ 'role' => $m['role'] === 'assistant' ? 'model' : 'user', 'parts' => [['text' => $m['content']]], ]; } $r = Http::withHeaders(['content-type' => 'application/json']) ->timeout(60) ->post('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=' . $key, [ 'systemInstruction' => ['parts' => [['text' => $this->buildSystemPrompt($company)]]], 'contents' => $contents, 'generationConfig' => ['maxOutputTokens' => 1024], ]); if (! $r->successful()) { return ['❌ Gemini error ' . $r->status() . ': ' . $r->body(), ['status' => $r->status()]]; } $body = $r->json(); $text = $body['candidates'][0]['content']['parts'][0]['text'] ?? '(răspuns gol)'; return [$text, [ 'model' => 'gemini-1.5-flash', 'tokens' => $body['usageMetadata'] ?? null, ]]; } }