get( 'ai_provider', 'wp_native_ai' ); switch ( $provider ) { case 'wp_native_ai': return new WordPressAIProvider(); case 'wp_connector_openai': case 'openai': return new OpenAIProvider(); case 'wp_connector_gemini': case 'wp_connector_google': case 'wp_connector_google-gemini': case 'gemini': return new GeminiProvider(); default: if ( WordPressAIProvider::is_available() ) { return new WordPressAIProvider(); } return new GeminiProvider(); } } /** * Run AI Support Assistant retrieval, construct prompt context, call LLM, and log result. * * @param string $session_id User session ID. * @param string $question User query. * @return array Answer and citation sources. */ public static function answer_question( string $session_id, string $question ): array { $settings = Settings::get_instance(); $provider = $settings->get( 'ai_provider', 'wp_native_ai' ); $model = 'default'; $api_key = ''; if ( 'wp_native_ai' === $provider ) { $model = 'wp-native-ai'; $has_native_or_keys = WordPressAIProvider::is_available() || ! empty( $settings->get( 'openai_api_key' ) ) || ! empty( $settings->get( 'gemini_api_key' ) ) || ! empty( ConnectorHelper::get_connector_api_key( 'google' ) ) || ! empty( ConnectorHelper::get_connector_api_key( 'openai' ) ); if ( ! $has_native_or_keys ) { $msg = __( 'No active AI provider credentials found. Please ensure WordPress Native AI Client is configured or enter your API key under AI Support Assistant Settings.', 'ai-support-assistant' ); ChatHistory::log( $session_id, $question, $msg, $provider, (string) $model ); return array( 'answer' => $msg, 'citations' => array(), ); } } elseif ( 'openai' === $provider || 'wp_connector_openai' === $provider ) { $model = 'gpt-4o-mini'; $api_key = $settings->get( 'openai_api_key', '' ); if ( empty( $api_key ) ) { $api_key = ConnectorHelper::get_connector_api_key( 'openai' ); } if ( empty( $api_key ) ) { $msg = __( 'OpenAI API key is missing. Please enter your API Key under AI Support Assistant -> Settings or configure WordPress Connectors.', 'ai-support-assistant' ); ChatHistory::log( $session_id, $question, $msg, $provider, (string) $model ); return array( 'answer' => $msg, 'citations' => array(), ); } } else { $model = $settings->get( 'gemini_chat_model', 'gemini-3.1-flash-lite' ); $valid_gemini_models = array( 'gemini-3.1-flash-lite' ); if ( ! in_array( $model, $valid_gemini_models, true ) ) { $model = 'gemini-3.1-flash-lite'; } $api_key = $settings->get( 'gemini_api_key', '' ); if ( empty( $api_key ) ) { $api_key = ConnectorHelper::get_connector_api_key( 'google' ); if ( empty( $api_key ) ) { $api_key = ConnectorHelper::get_connector_api_key( 'gemini' ); } } if ( empty( $api_key ) ) { $msg = __( 'Google Gemini API key is missing. Please enter your API Key under AI Support Assistant -> Settings or configure WordPress Connectors.', 'ai-support-assistant' ); ChatHistory::log( $session_id, $question, $msg, $provider, (string) $model ); return array( 'answer' => $msg, 'citations' => array(), ); } } $history = array(); if ( $settings->get( 'enable_chat_history', 1 ) ) { $history = ChatHistory::get_history( $session_id, 5 ); } Logger::info( 'AI Support Assistant requested', array( 'session_id' => $session_id, 'question' => $question ) ); // 1. Retrieve most relevant chunks. $chunks = RetrievalService::retrieve( $question ); // If no context documents are available, respond with the fallback directly. $fallback_msg = $settings->get( 'chat_fallback_message', __( "I couldn't find that information in the uploaded documentation.", 'ai-support-assistant' ) ); if ( empty( $chunks ) ) { ChatHistory::log( $session_id, $question, $fallback_msg, $provider, (string) $model ); return array( 'answer' => $fallback_msg, 'citations' => array(), ); } // 2. Generate response via LLM provider. $active_provider = self::get_provider(); $answer = $active_provider->generate_response( $chunks, $question, $history ); // 3. Log query to database. ChatHistory::log( $session_id, $question, $answer, $provider, (string) $model ); // 4. Format citations (deduplicated by document). $citations = array(); if ( $settings->get( 'enable_citations', 1 ) ) { $seen_docs = array(); foreach ( $chunks as $chunk ) { $doc_key = ! empty( $chunk['filename'] ) ? $chunk['filename'] : ( ! empty( $chunk['title'] ) ? $chunk['title'] : ( 'doc_' . ( $chunk['document_id'] ?? '0' ) ) ); if ( ! isset( $seen_docs[ $doc_key ] ) ) { $seen_docs[ $doc_key ] = array( 'document_id' => $chunk['document_id'] ?? 0, 'title' => $chunk['title'] ?? '', 'filename' => $chunk['filename'] ?? '', 'chunk_number' => $chunk['chunk_number'] ?? 1, 'page_numbers' => array(), 'similarity' => round( $chunk['similarity'] ?? 0, 3 ), ); } if ( ! empty( $chunk['page_number'] ) && ! in_array( $chunk['page_number'], $seen_docs[ $doc_key ]['page_numbers'], true ) ) { $seen_docs[ $doc_key ]['page_numbers'][] = $chunk['page_number']; } } foreach ( $seen_docs as $doc_data ) { if ( ! empty( $doc_data['page_numbers'] ) ) { sort( $doc_data['page_numbers'], SORT_NUMERIC ); $doc_data['page_number'] = implode( ', ', $doc_data['page_numbers'] ); } else { $doc_data['page_number'] = null; } unset( $doc_data['page_numbers'] ); $citations[] = $doc_data; } } return array( 'answer' => $answer, 'citations' => $citations, ); } }