get_settings(); // 1. Try Google / Gemini connector from WordPress AI Client registry. $google_key = ConnectorHelper::get_connector_api_key( 'google' ); if ( empty( $google_key ) ) { $google_key = ConnectorHelper::get_connector_api_key( 'gemini' ); } if ( ! empty( $google_key ) ) { try { $gemini_provider = new GeminiProvider( $google_key ); return $gemini_provider->get_embedding( $text ); } catch ( \Throwable $e ) { Logger::warning( 'Google connector embedding attempt failed: ' . $e->getMessage() ); } } // 2. Try OpenAI connector from WordPress AI Client registry. $openai_key = ConnectorHelper::get_connector_api_key( 'openai' ); if ( ! empty( $openai_key ) ) { try { $openai_provider = new OpenAIProvider( $openai_key ); return $openai_provider->get_embedding( $text ); } catch ( \Throwable $e ) { Logger::warning( 'OpenAI connector embedding attempt failed: ' . $e->getMessage() ); } } // 3. Try direct Gemini key from plugin settings. $direct_gemini = $settings->get_secret( 'gemini_api_key' ); if ( ! empty( $direct_gemini ) ) { try { $gemini_provider = new GeminiProvider( $direct_gemini ); return $gemini_provider->get_embedding( $text ); } catch ( \Throwable $e ) { Logger::warning( 'Gemini direct embedding attempt failed: ' . $e->getMessage() ); } } // 4. Try direct OpenAI key from plugin settings. $direct_openai = $settings->get_secret( 'openai_api_key' ); if ( ! empty( $direct_openai ) ) { try { $openai_provider = new OpenAIProvider( $direct_openai ); return $openai_provider->get_embedding( $text ); } catch ( \Throwable $e ) { Logger::warning( 'OpenAI direct embedding attempt failed: ' . $e->getMessage() ); } } Logger::error( 'No API key configured for embedding generation.' ); throw new \Exception( esc_html__( 'Embedding generation requires an active AI connector or API key. Please configure a connector in WordPress Connectors or enter your API key under AI Support Assistant Settings.', 'ai-support-assistant' ) ); } /** * Generate a chat completion using the WordPress Native AI Client with context injected. * * @param array $chunks Array of relevant document chunks. * @param string $question User query text. * @param array $history Recent conversation history. * @return string The generated assistant response. */ public function generate_response( array $chunks, string $question, array $history = array(), ?callable $on_delta = null ): string { $emitted = false; if ( $on_delta ) { $callback = $on_delta; $on_delta = static function ( $text ) use ( $callback, &$emitted ) { $emitted = true; $callback( $text ); }; } $settings = $this->get_settings(); $temp = (float) $settings->get( 'temperature', 0.2 ); $max_t = (int) $settings->get( 'max_tokens', 1000 ); // Format context documentation blocks. $context_str = ''; foreach ( $chunks as $chunk ) { $page_info = ! empty( $chunk['page_number'] ) ? ' | Page: ' . $chunk['page_number'] : ''; $context_str .= sprintf( "[Document: %s%s]\n%s\n---\n", $chunk['filename'] ?? 'Unknown', $page_info, $chunk['chunk_text'] ); } // Compile system instruction. $custom_instruction = $settings->get( 'ai_system_instruction', "You are a professional customer support assistant.\nAnswer using ONLY the supplied documentation.\nFormat your response cleanly using structured Markdown bullet points (* or -) and bold section titles so it is clean, elegant, and easy to read.\nIf the answer is not found, say:\n\"I couldn't find that information in the uploaded documentation.\"\nDo not invent information." ); $system_prompt = $custom_instruction . "\n\nDocumentation:\n" . $context_str; // Build dialogue context. $full_prompt = ''; if ( ! empty( $history ) ) { $full_prompt .= "Recent conversation history:\n"; foreach ( $history as $turn ) { $full_prompt .= 'User: ' . $turn['question'] . "\nAssistant: " . $turn['answer'] . "\n"; } $full_prompt .= "\n"; } $full_prompt .= 'Question: ' . $question; Logger::debug( 'Requesting WordPress Native AI Chat response' ); $last_error = null; // 1. Try native wp_ai_client_prompt(). if ( function_exists( 'wp_ai_client_prompt' ) ) { try { $builder = wp_ai_client_prompt( $full_prompt ); foreach ( array( array( 'using_system_instruction', 'usingSystemInstruction', $system_prompt ), array( 'using_temperature', 'usingTemperature', $temp ), array( 'using_max_tokens', 'usingMaxTokens', $max_t ), ) as $option ) { $method = self::builder_method( $builder, $option[0], $option[1] ); if ( $method ) { $builder->$method( $option[2] ); } } // Prefer the administrator's selected model instead of an arbitrary registry default. $preferences = array(); if ( ConnectorHelper::get_connector_api_key( 'google' ) ) { $model = trim( (string) $settings->get( 'gemini_chat_model', '' ) ); $preferences[] = array( 'google', '' !== $model ? $model : 'gemini-3.1-flash-lite' ); } if ( ConnectorHelper::get_connector_api_key( 'openai' ) ) { $model = trim( (string) $settings->get( 'openai_chat_model', '' ) ); $preferences[] = array( 'openai', '' !== $model ? $model : 'gpt-4o-mini' ); } $method = self::builder_method( $builder, 'using_model_preference', 'usingModelPreference' ); if ( $preferences && $method ) { $builder->$method( ...$preferences ); } $method = self::builder_method( $builder, 'generate_text', 'generateText' ); if ( ! $method ) { throw new \RuntimeException( 'The native AI client does not support text generation.' ); } $response_text = $builder->$method(); if ( is_wp_error( $response_text ) ) { throw new \RuntimeException( esc_html( $response_text->get_error_message() ) ); } if ( ! is_string( $response_text ) || '' === trim( $response_text ) ) { throw new \RuntimeException( 'The native AI client returned an empty answer.' ); } if ( $on_delta ) { $on_delta( trim( $response_text ) ); } return trim( $response_text ); } catch ( \Throwable $e ) { if ( $emitted ) { throw $e; } $last_error = $e; Logger::warning( 'wp_ai_client_prompt failed, attempting connector/plugin fallback: ' . $e->getMessage() ); } } // 2. Fallback to Google / Gemini connector key from AI Client registry. $google_key = ConnectorHelper::get_connector_api_key( 'google' ); if ( empty( $google_key ) ) { $google_key = ConnectorHelper::get_connector_api_key( 'gemini' ); } if ( ! empty( $google_key ) ) { try { return ( new GeminiProvider( $google_key ) )->generate_response( $chunks, $question, $history, $on_delta ); } catch ( \Throwable $e ) { if ( $emitted ) { throw $e; } $last_error = $e; Logger::warning( 'Google connector chat attempt failed: ' . $e->getMessage() ); } } // 3. Fallback to OpenAI connector key from AI Client registry. $openai_key = ConnectorHelper::get_connector_api_key( 'openai' ); if ( ! empty( $openai_key ) ) { try { return ( new OpenAIProvider( $openai_key ) )->generate_response( $chunks, $question, $history, $on_delta ); } catch ( \Throwable $e ) { if ( $emitted ) { throw $e; } $last_error = $e; Logger::warning( 'OpenAI connector chat attempt failed: ' . $e->getMessage() ); } } // 4. Fallback to direct keys in plugin settings. $direct_gemini = $settings->get_secret( 'gemini_api_key' ); if ( ! empty( $direct_gemini ) ) { try { return ( new GeminiProvider( $direct_gemini ) )->generate_response( $chunks, $question, $history, $on_delta ); } catch ( \Throwable $e ) { if ( $emitted ) { throw $e; } $last_error = $e; Logger::warning( 'Direct Gemini chat attempt failed: ' . $e->getMessage() ); } } $direct_openai = $settings->get_secret( 'openai_api_key' ); if ( ! empty( $direct_openai ) ) { try { return ( new OpenAIProvider( $direct_openai ) )->generate_response( $chunks, $question, $history, $on_delta ); } catch ( \Throwable $e ) { if ( $emitted ) { throw $e; } $last_error = $e; Logger::warning( 'Direct OpenAI chat attempt failed: ' . $e->getMessage() ); } } if ( $last_error ) { throw $last_error; } throw new \RuntimeException( 'No available AI provider completed the chat request. Check provider credentials and connection.', 0, $last_error // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Throwable passed as the previous exception, not a message or output. ); } /** WordPress forwards snake_case methods through __call; method_exists misses them. */ private static function builder_method( $builder, string $snake_case, string $camel_case ): string { if ( is_callable( array( $builder, $snake_case ) ) ) { return $snake_case; } return is_callable( array( $builder, $camel_case ) ) ? $camel_case : ''; } }