hasLocalKey() || $this->hasPlatformRelay(); } /** * @param array $history * @param array $context */ public function chat(string $message, array $history, array $context): string { if (! $this->enabled()) { throw new RuntimeException('Afia is not configured.'); } if ($this->hasLocalKey()) { return $this->chatLocally($message, $history, $context); } return $this->chatViaPlatform($message, $history, $context); } /** * @param array $history * @param array $context */ private function chatLocally(string $message, array $history, array $context): string { $provider = (string) config('afia.provider', 'openai'); $model = (string) config('afia.model', 'gpt-4o-mini'); $apiKey = (string) config('afia.api_key'); $messages = [['role' => 'system', 'content' => $this->systemPrompt($context)]]; foreach (array_slice($history, -8) as $turn) { $role = ($turn['role'] ?? 'user') === 'assistant' ? 'assistant' : 'user'; $text = trim((string) ($turn['text'] ?? '')); if ($text !== '') { $messages[] = ['role' => $role, 'content' => $text]; } } $messages[] = ['role' => 'user', 'content' => $message]; return $provider === 'anthropic' ? $this->viaAnthropic($model, $apiKey, $messages) : $this->viaOpenAi($model, $apiKey, $messages); } /** * @param array $history * @param array $context */ private function chatViaPlatform(string $message, array $history, array $context): string { $base = rtrim((string) config('afia.platform_api_url', ''), '/'); $token = (string) config('afia.platform_api_key', ''); $res = Http::withToken($token)->acceptJson()->timeout(50)->post($base.'/afia/chat', [ 'product' => (string) config('afia.product', 'care'), 'message' => $message, 'history' => $history, 'system_prompt' => $this->systemPrompt($context), ]); if ($res->status() === 503) { throw new RuntimeException('Afia is not configured on the platform.'); } if ($res->failed()) { throw new RuntimeException('Platform Afia relay failed: '.$res->status()); } $reply = trim((string) $res->json('reply', '')); if ($reply === '') { throw new RuntimeException('Platform Afia relay returned an empty response.'); } return $reply; } private function hasLocalKey(): bool { return (string) config('afia.api_key', '') !== ''; } private function hasPlatformRelay(): bool { return rtrim((string) config('afia.platform_api_url', ''), '/') !== '' && (string) config('afia.platform_api_key', '') !== ''; } private function viaOpenAi(string $model, string $apiKey, array $messages): string { $res = Http::withToken($apiKey)->acceptJson()->timeout(45) ->post('https://api.openai.com/v1/chat/completions', [ 'model' => $model, 'temperature' => 0.3, 'max_tokens' => 600, 'messages' => $messages, ]); if ($res->failed()) { throw new RuntimeException('OpenAI request failed: '.$res->status()); } return trim((string) $res->json('choices.0.message.content', '')); } private function viaAnthropic(string $model, string $apiKey, array $messages): string { $system = $messages[0]['content'] ?? ''; $turns = array_values(array_filter($messages, fn ($m) => $m['role'] !== 'system')); $res = Http::withHeaders([ 'x-api-key' => $apiKey, 'anthropic-version' => '2023-06-01', ])->acceptJson()->timeout(45)->post('https://api.anthropic.com/v1/messages', [ 'model' => $model, 'max_tokens' => 600, 'system' => $system, 'messages' => array_map(fn ($m) => ['role' => $m['role'], 'content' => $m['content']], $turns), ]); if ($res->failed()) { throw new RuntimeException('Anthropic request failed: '.$res->status()); } return trim((string) $res->json('content.0.text', '')); } /** @param array $context */ private function systemPrompt(array $context): string { $ctx = collect($context)->map(fn ($v, $k) => "- {$k}: {$v}")->implode("\n"); return <<