custom_api_key = $custom_api_key; } /** * Retrieve settings instance. * * @return Settings */ private function get_settings(): Settings { return Settings::get_instance(); } /** * Fetch API Key. * * @return string */ private function get_api_key(): string { if ( ! empty( $this->custom_api_key ) ) { return $this->custom_api_key; } $settings = $this->get_settings(); $direct_key = (string) $settings->get( 'gemini_api_key', '' ); if ( ! empty( $direct_key ) ) { return $direct_key; } $connector_key = ConnectorHelper::get_connector_api_key( 'google' ); if ( empty( $connector_key ) ) { $connector_key = ConnectorHelper::get_connector_api_key( 'gemini' ); } return $connector_key; } /** * Retrieve vector embedding representation of text using gemini-embedding-001. * * @param string $text Input text block. * @return array Float vector array. */ public function get_embedding( string $text ): array { $api_key = $this->get_api_key(); if ( empty( $api_key ) ) { Logger::error( 'Gemini API key is missing. Cannot generate embedding.' ); throw new \Exception( esc_html__( 'Gemini API key is missing. Please save your API key in settings.', 'ai-support-assistant' ) ); } $model = $this->get_settings()->get( 'gemini_embedding_model', 'gemini-embedding-001' ); $valid_embed_models = array( 'gemini-embedding-001' ); if ( ! in_array( $model, $valid_embed_models, true ) ) { $model = 'gemini-embedding-001'; } // Gemini Embed API endpoint. // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration provided as optional fallback alongside WordPress Native AI Client. $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':embedContent?key=' . $api_key; $body = array( 'model' => 'models/' . $model, 'content' => array( 'parts' => array( array( 'text' => $text ), ), ), ); Logger::debug( 'Generating Gemini embedding', array( 'model' => $model ) ); $response = wp_safe_remote_post( $url, array( 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $body ), 'timeout' => 30, ) ); if ( is_wp_error( $response ) ) { $err_msg = $response->get_error_message(); Logger::error( 'Gemini Embedding API error: ' . $err_msg ); /* translators: %s: Gemini connection error message. */ throw new \Exception( esc_html( sprintf( __( 'Gemini API connection error: %s', 'ai-support-assistant' ), $err_msg ) ) ); } $code = wp_remote_retrieve_response_code( $response ); $res_body = wp_remote_retrieve_body( $response ); if ( 200 !== $code ) { Logger::error( "Gemini Embedding API error (HTTP $code)", array( 'response' => $res_body ) ); $decoded = json_decode( $res_body, true ); $err_msg = ! empty( $decoded['error']['message'] ) ? $decoded['error']['message'] : "HTTP $code"; /* translators: %s: Gemini API error details. */ throw new \Exception( esc_html( sprintf( __( 'Gemini API error: %s', 'ai-support-assistant' ), $err_msg ) ) ); } $decoded = json_decode( $res_body, true ); if ( ! empty( $decoded['embedding']['values'] ) ) { return $decoded['embedding']['values']; } Logger::error( 'Gemini Embedding response is malformed.', array( 'response' => $res_body ) ); throw new \Exception( esc_html__( 'Gemini Embedding response is malformed.', 'ai-support-assistant' ) ); } /** * Generate chat response using Gemini models. * * @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() ): string { $api_key = $this->get_api_key(); if ( empty( $api_key ) ) { return __( "I couldn't process this query. Gemini API Key is missing.", 'ai-support-assistant' ); } $settings = $this->get_settings(); $model = $settings->get( 'gemini_chat_model', 'gemini-3.1-flash-lite' ); $valid_chat_models = array( 'gemini-3.1-flash-lite' ); if ( ! in_array( $model, $valid_chat_models, true ) ) { $model = 'gemini-3.1-flash-lite'; } $temp = $settings->get( 'temperature', 0.2 ); $max_t = $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; $contents = array(); // Hydrate dialogue history (if exists). Note: Gemini roles are 'user' and 'model'. if ( ! empty( $history ) ) { foreach ( $history as $turn ) { $contents[] = array( 'role' => 'user', 'parts' => array( array( 'text' => $turn['question'] ), ), ); $contents[] = array( 'role' => 'model', 'parts' => array( array( 'text' => $turn['answer'] ), ), ); } } // Append the actual query. $contents[] = array( 'role' => 'user', 'parts' => array( array( 'text' => "Question: $question" ), ), ); // Gemini GenerateContent endpoint. // phpcs:ignore PluginCheck.CodeAnalysis.AIProvider.DirectIntegration -- Direct integration provided as optional fallback alongside WordPress Native AI Client. $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . $api_key; $body = array( 'contents' => $contents, 'systemInstruction' => array( 'parts' => array( array( 'text' => $system_prompt ), ), ), 'generationConfig' => array( 'temperature' => $temp, 'maxOutputTokens' => $max_t, ), ); Logger::debug( 'Requesting Gemini Chat response', array( 'model' => $model ) ); $response = wp_safe_remote_post( $url, array( 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $body ), 'timeout' => 45, ) ); if ( is_wp_error( $response ) ) { Logger::error( 'Gemini Chat API error: ' . $response->get_error_message() ); return __( 'Sorry, an error occurred with the AI service.', 'ai-support-assistant' ); } $code = wp_remote_retrieve_response_code( $response ); $res_body = wp_remote_retrieve_body( $response ); if ( 200 !== $code ) { Logger::error( "Gemini Chat API error (HTTP $code)", array( 'response' => $res_body ) ); $decoded = json_decode( $res_body, true ); if ( ! empty( $decoded['error']['message'] ) ) { $msg = $decoded['error']['message']; if ( 429 === $code || false !== strpos( strtolower( $msg ), 'quota' ) ) { return __( 'Google Gemini API Quota Exceeded: Your Google API Key has exceeded its usage quota. Please generate a new API key or check your quota at https://ai.google.dev.', 'ai-support-assistant' ); } /* translators: %s: Gemini API error message. */ return sprintf( __( 'Google Gemini API Error: %s', 'ai-support-assistant' ), $msg ); } return __( 'Sorry, the AI service returned an error.', 'ai-support-assistant' ); } $decoded = json_decode( $res_body, true ); if ( ! empty( $decoded['candidates'][0]['content']['parts'][0]['text'] ) ) { return trim( $decoded['candidates'][0]['content']['parts'][0]['text'] ); } Logger::error( 'Gemini Chat Response is malformed.', array( 'response' => $res_body ) ); return __( 'Sorry, a malformed response was received from the AI service.', 'ai-support-assistant' ); } }