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, string $override = '', ?callable $on_delta = null ): array { return SessionLock::run( $session_id, static function () use ( $session_id, $question, $override, $on_delta ) { return self::generate_answer( $session_id, $question, $override, $on_delta ); } ); } private static function generate_answer( string $session_id, string $question, string $override, ?callable $on_delta ): array { $settings = Settings::get_instance(); $provider = $override ?: $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_secret( 'openai_api_key' ) ) || ! empty( $settings->get_secret( 'gemini_api_key' ) ) || ! empty( ConnectorHelper::get_connector_api_key( 'google' ) ) || ! empty( ConnectorHelper::get_connector_api_key( 'openai' ) ); if ( ! $has_native_or_keys ) { throw new \RuntimeException( 'No active AI provider credentials found.' ); } } elseif ( 'openai' === $provider || 'wp_connector_openai' === $provider ) { $model = $settings->get( 'openai_chat_model', '' ) ?: 'gpt-4o-mini'; $api_key = $settings->get_secret( 'openai_api_key' ); if ( empty( $api_key ) ) { $api_key = ConnectorHelper::get_connector_api_key( 'openai' ); } if ( empty( $api_key ) ) { throw new \RuntimeException( 'OpenAI API key is missing.' ); } } else { $model = $settings->get( 'gemini_chat_model', '' ) ?: 'gemini-3.1-flash-lite'; $api_key = $settings->get_secret( '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 ) ) { throw new \RuntimeException( 'Google Gemini API key is missing.' ); } } $history = array(); if ( $settings->get( 'enable_chat_history', 1 ) ) { $history = ChatHistory::get_history( $session_id, 5 ); } Logger::debug( 'AI Support Assistant requested.' ); // 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, 'fallback' ); if ( $on_delta ) { $on_delta( $fallback_msg ); } return array( 'answer' => $fallback_msg, 'citations' => array(), ); } // 2. Generate response via LLM provider. $active_provider = self::get_provider( $override ); $answer = $active_provider->generate_response( $chunks, $question, $history, $on_delta ); // 3. Log query to database. $outcome = trim( $answer ) === trim( $fallback_msg ) ? 'fallback' : 'completed'; ChatHistory::log( $session_id, $question, $answer, $provider, (string) $model, $outcome ); // 4. Format citations (deduplicated by document). $citations = array(); if ( 'fallback' !== $outcome && $settings->get( 'enable_citations', 1 ) ) { $seen_docs = array(); foreach ( $chunks as $chunk ) { $doc_key = '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['score'] ?? 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, ); } }