connection['id']) ? 'connection:'.$this->connection['id'] : 'openai'; } public function external(): bool { return ($this->connection['mode'] ?? 'online') === 'online'; } public function configured(): bool { if ($this->connection) { return filled($this->connection['default_model'] ?? null) && (! $this->external() || filled($this->apiKey())); } return filled($this->baseUrl()) && filled($this->apiKey()) && filled(config('ai.openai.models.balanced')); } public function chat(string $prompt, ?string $systemPrompt = null): string { if (! $this->configured()) { throw new RuntimeException('ارائه‌دهنده انتخاب‌شده هنوز تنظیمات معتبر ندارد.'); } $messages = []; if (filled($systemPrompt)) { $messages[] = ['role' => 'system', 'content' => $systemPrompt]; } $messages[] = ['role' => 'user', 'content' => $prompt]; $response = $this->client($this->timeout())->asJson()->retry(2, 500, throw: false)->post($this->baseUrl().'/chat/completions', [ 'model' => $this->model('balanced'), 'messages' => $messages, ]); if (! $response->successful()) { throw new RuntimeException('AI provider request failed with status '.$response->status().'.'); } $answer = trim((string) $response->json('choices.0.message.content')); if ($answer === '') { throw new RuntimeException('AI provider returned an empty response.'); } return $answer; } public function health(): array { if (! $this->configured()) { return ['connected' => false, 'message' => 'کلید دسترسی و مدل پیش‌فرض اتصال را تکمیل کنید.']; } try { $started = microtime(true); $response = $this->client(15)->get($this->baseUrl().'/models'); return ['connected' => $response->successful(), 'latencyMs' => (int) round((microtime(true) - $started) * 1000), 'message' => $response->successful() ? 'اتصال برقرار است.' : 'ارائه‌دهنده پاسخ معتبر نداد.']; } catch (\Throwable) { return ['connected' => false, 'message' => 'اتصال با ارائه‌دهنده برقرار نشد.']; } } public function structure(array $fragments, array $options): array { $source = collect($fragments)->map(fn (array $item) => ['id' => $item['id'], 'heading' => $item['heading'], 'content' => mb_substr($item['content'], 0, 12000)])->all(); $instruction = 'Create a source-grounded microlearning course draft. Return JSON only with: schemaVersion, providerDisclosure, title, description, settings, modules[].title, modules[].lessons[].title, summary, sourceFragmentIds, blocks[].type, schemaVersion, data. Use only text blocks with data.html. Preserve supplied fragment IDs. Never publish automatically.'; $payload = $this->complete($instruction, ['source' => $source, 'options' => $options], (string) ($options['modelPolicy'] ?? 'balanced')); abort_unless(isset($payload['title'], $payload['modules']) && is_array($payload['modules']), 502, 'پاسخ هوش مصنوعی ساختار معتبر دوره را نداشت.'); $payload['schemaVersion'] = 1; $payload['providerDisclosure'] = 'Generated by the configured OpenAI-compatible provider; human review is required.'; return $payload; } public function assist(string $operation, string $content, array $context): array { $modelPolicy = (string) ($context['modelPolicy'] ?? 'fast'); unset($context['modelPolicy']); return ['schemaVersion' => 1, 'operation' => $operation, 'proposal' => $this->complete('Perform the requested course-authoring operation. Return JSON only. Do not invent facts not present in the content.', ['operation' => $operation, 'content' => $content, 'context' => $context], $modelPolicy), 'providerDisclosure' => 'OpenAI-compatible provider; review before accepting.']; } /** @return array */ private function complete(string $system, array $input, string $modelPolicy): array { if (! $this->configured()) { throw new RuntimeException('ارائه‌دهنده انتخاب‌شده هنوز کلید دسترسی معتبر ندارد.'); } $response = $this->client($this->timeout())->asJson()->retry(2, 500, throw: false)->post($this->baseUrl().'/chat/completions', [ 'model' => $this->model($modelPolicy), 'temperature' => 0.2, 'response_format' => ['type' => 'json_object'], 'messages' => [['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => json_encode($input, JSON_UNESCAPED_UNICODE)]], ]); if (! $response->successful()) { throw new RuntimeException('AI provider request failed with status '.$response->status().'.'); } $content = (string) $response->json('choices.0.message.content'); $decoded = json_decode($content, true); if (! is_array($decoded)) { throw new RuntimeException('AI provider returned invalid JSON.'); } return $decoded; } private function client(int $timeout): PendingRequest { $client = Http::acceptJson() ->timeout($timeout) ->withOptions($this->endpointPolicy->requestOptions( (string) ($this->connection['provider'] ?? 'openai'), (string) ($this->connection['mode'] ?? 'online'), $this->baseUrl(), )); if (filled($this->apiKey())) { $client = $client->withToken($this->apiKey()); } return $client; } private function baseUrl(): string { return rtrim((string) ($this->connection['base_url'] ?? config('ai.openai.base_url')), '/'); } private function apiKey(): string { return (string) ($this->connection['api_key'] ?? config('ai.openai.api_key')); } private function model(string $policy): string { if ($this->connection) { return in_array($policy, ['fast', 'balanced', 'advanced'], true) ? (string) ($this->connection['default_model'] ?? '') : $policy; } return (string) config("ai.openai.models.{$policy}", config('ai.openai.models.balanced')); } private function timeout(): int { return (int) ($this->connection['timeout_seconds'] ?? config('ai.openai.timeout', 90)); } }