table_name = $wpdb->prefix . 'aiohm_vector_entries'; $this->allowed_tables = array( $wpdb->prefix . 'aiohm_vector_entries', $wpdb->prefix . 'aiohm_conversations', $wpdb->prefix . 'aiohm_messages', $wpdb->prefix . 'aiohm_projects', ); } public function get_table_name() { return $this->table_name; } /** * Validate table name against allowed tables to prevent SQL injection * * @param string $table_name Table name to validate * @return bool True if valid, false otherwise */ private function validate_table_name( $table_name ) { return in_array( $table_name, $this->allowed_tables, true ); } /** * Safely get table reference with validation * * @param string $table_name Table name to validate * @return string Validated table name * @throws Exception If table name is invalid */ private function get_safe_table_name( $table_name ) { if ( ! $this->validate_table_name( $table_name ) ) { throw new Exception( 'Invalid table name for security reasons.' ); } return $table_name; } public function add_entry( $content, $content_type, $title, $metadata = array(), $user_id = 0, $is_public = 0, $attachment_id = 0 ) { $settings = AIOHM_KB_Assistant::get_settings(); $chunk_size = $settings['chunk_size'] ?? 1000; $chunk_overlap = $settings['chunk_overlap'] ?? 200; $chunks = $this->chunk_content( $content, $chunk_size, $chunk_overlap ); if ( empty( $chunks ) ) { return 0; } $transient_dir = $this->get_transient_directory(); $chunks_file_path = $transient_dir . '/' . $attachment_id . '_chunks.json'; file_put_contents( $chunks_file_path, json_encode( $chunks ) ); $content_id = $this->generate_entry_id( $title, $content ); // Store content_id in attachment meta to retrieve it later update_post_meta( $attachment_id, '_aiohm_content_id', $content_id ); return count( $chunks ); } /** * Prepare content for chunked processing (public helper) * * @param string $content Content to chunk * @param string $title Content title * @param int $attachment_id Attachment ID for metadata * @return array Array with chunks and content_id */ public function prepare_content_for_chunking( $content, $title, $attachment_id = 0 ) { $settings = AIOHM_KB_Assistant::get_settings(); $chunk_size = $settings['chunk_size'] ?? 1000; $chunk_overlap = $settings['chunk_overlap'] ?? 200; $chunks = $this->chunk_content( $content, $chunk_size, $chunk_overlap ); $content_id = $this->generate_entry_id( $title, $content ); return array( 'chunks' => $chunks, 'content_id' => $content_id, 'chunk_count' => count( $chunks ), ); } public function add_entry_chunk( $attachment_id, $chunk_index ) { $transient_dir = $this->get_transient_directory(); $chunks_file_path = $transient_dir . '/' . $attachment_id . '_chunks.json'; if ( ! file_exists( $chunks_file_path ) ) { throw new Exception( 'Chunks file not found.' ); } $chunks = json_decode( file_get_contents( $chunks_file_path ), true ); if ( ! $chunks || ! isset( $chunks[ $chunk_index ] ) ) { throw new Exception( 'Invalid chunk data.' ); } $chunk = $chunks[ $chunk_index ]; $attachment = get_post( $attachment_id ); $title = $attachment->post_title; $content_id = get_post_meta( $attachment_id, '_aiohm_content_id', true ); $user_id = $attachment->post_author; $ai_client = new AIOHM_KB_AI_GPT_Client(); $embedding = $ai_client->generate_embeddings( $chunk ); if ( ! $embedding || empty( $embedding ) ) { throw new Exception( 'Failed to generate embeddings.' ); } $metadata = array( 'chunk_index' => $chunk_index, 'attachment_id' => $attachment_id, ); global $wpdb; $insert_data = array( 'user_id' => $user_id, 'content_id' => $content_id, 'content_type' => get_post_mime_type( $attachment_id ), 'title' => $title, 'content' => $chunk, 'vector_data' => json_encode( $embedding ), 'metadata' => json_encode( $metadata ), 'is_public' => 0, ); // Direct database query required for vector embeddings table - no WordPress equivalent exists // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $result = $wpdb->insert( $this->table_name, $insert_data, array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d' ) ); if ( $result ) { } else { throw new Exception( 'Failed to insert chunk into database: ' . esc_html( $wpdb->last_error ) ); } } private function get_transient_directory() { $upload_dir = wp_upload_dir(); $transient_dir = $upload_dir['basedir'] . '/aiohm-transients'; if ( ! file_exists( $transient_dir ) ) { wp_mkdir_p( $transient_dir ); } return $transient_dir; } public function query( $query_text, $scope = 'site', $user_id = 0 ) { try { $research_prefix = 'Please research the following URL and provide a summary of its key points:'; if ( strpos( $query_text, $research_prefix ) === 0 ) { preg_match( '/(https?:\/\/[^\s]+)/', $query_text, $matches ); $url = $matches[0] ?? null; if ( ! $url ) { return "I couldn't find a valid URL in your request. Please try again with the full URL (e.g., https://example.com)."; } $result = $this->research_and_add_url( $url, $user_id ); if ( is_wp_error( $result ) ) { return 'I encountered an error trying to research that URL: ' . $result->get_error_message(); } return $this->summarize_new_context( $url, $user_id ); } if ( $scope === 'private' ) { $context_entries = $this->find_context_for_user( $query_text, $user_id ); } else { $context_entries = $this->find_relevant_context( $query_text ); } $context = ''; foreach ( $context_entries as $entry ) { $context .= 'Title: ' . $entry['entry']['title'] . "\n"; $context .= 'Content: ' . $entry['entry']['content'] . "\n\n"; } $settings = AIOHM_KB_Assistant::get_settings(); $ai_settings = ( $scope === 'private' ) ? $settings['muse_mode'] : $settings['mirror_mode']; $system_message_key = ( $scope === 'private' ) ? 'system_prompt' : 'qa_system_message'; $system_message = $ai_settings[ $system_message_key ] ?? 'You are a helpful assistant.'; $model_name = $ai_settings['ai_model'] ?? 'gpt-3.5-turbo'; $temperature = $ai_settings['temperature'] ?? 0.7; $enriched_user_message = "Here is some context to help you answer:\n\n---\n\n{$context}\n\n---\n\nBased on that context, please answer the following question:\n\n{$query_text}"; $ai_client = new AIOHM_KB_AI_GPT_Client(); return $ai_client->get_chat_completion( $system_message, $enriched_user_message, $temperature, $model_name ); } catch ( Exception $e ) { return 'AI Error: ' . $e->getMessage(); } } public function research_and_add_url( $url, $user_id ) { if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { return new WP_Error( 'invalid_url', 'The provided URL is not valid.' ); } // Enhanced request with proper headers $response = wp_remote_get( $url, array( 'timeout' => 30, 'headers' => array( 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language' => 'en-US,en;q=0.5', 'Accept-Encoding' => 'gzip, deflate', 'Connection' => 'keep-alive', 'Upgrade-Insecure-Requests' => '1', ), ) ); if ( is_wp_error( $response ) ) { return new WP_Error( 'fetch_failed', 'Could not retrieve content: ' . $response->get_error_message() ); } $content_type = wp_remote_retrieve_header( $response, 'content-type' ); if ( strpos( $content_type, 'text/html' ) === false ) { return new WP_Error( 'invalid_content_type', 'The URL does not appear to be an HTML page.' ); } $html_content = wp_remote_retrieve_body( $response ); // Enhanced content extraction $extracted_data = $this->extract_enhanced_content( $html_content, $url ); if ( empty( trim( $extracted_data['content'] ) ) ) { return new WP_Error( 'no_content', 'Could not extract any readable text from the URL.' ); } $metadata = array( 'source_url' => $url, 'title' => $extracted_data['title'], 'description' => $extracted_data['description'], 'author' => $extracted_data['author'], 'published_date' => $extracted_data['published_date'], 'keywords' => $extracted_data['keywords'], 'extraction_date' => current_time( 'mysql' ), ); return $this->add_entry( $extracted_data['content'], 'external_url', $extracted_data['title'], $metadata, $user_id, 0 ); } private function extract_enhanced_content( $html_content, $url ) { $title = 'Web Research: ' . $url; $description = ''; $author = ''; $published_date = ''; $keywords = ''; // Create DOMDocument for better parsing $dom = new DOMDocument(); @$dom->loadHTML( '' . $html_content ); $xpath = new DOMXPath( $dom ); // Extract title - try multiple methods if ( preg_match( '/(.*?)<\/title>/i', $html_content, $matches ) ) { $title = trim( html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' ) ); } if ( preg_match( '/<meta\s+name=["\']description["\']\s+content=["\'](.*?)["\']/i', $html_content, $matches ) ) { $description = trim( html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' ) ); } if ( preg_match( '/<meta\s+property=["\']og:title["\']\s+content=["\'](.*?)["\']/i', $html_content, $matches ) ) { $og_title = trim( html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' ) ); if ( ! empty( $og_title ) && strlen( $og_title ) > strlen( $title ) ) { $title = $og_title; } } if ( preg_match( '/<meta\s+property=["\']og:description["\']\s+content=["\'](.*?)["\']/i', $html_content, $matches ) ) { $og_description = trim( html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' ) ); if ( ! empty( $og_description ) && empty( $description ) ) { $description = $og_description; } } if ( preg_match( '/<meta\s+name=["\']author["\']\s+content=["\'](.*?)["\']/i', $html_content, $matches ) ) { $author = trim( html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' ) ); } if ( preg_match( '/<meta\s+name=["\']keywords["\']\s+content=["\'](.*?)["\']/i', $html_content, $matches ) ) { $keywords = trim( html_entity_decode( $matches[1], ENT_QUOTES, 'UTF-8' ) ); } if ( preg_match( '/<meta\s+property=["\']article:published_time["\']\s+content=["\'](.*?)["\']/i', $html_content, $matches ) ) { $published_date = trim( $matches[1] ); } $content_selectors = array( 'article', 'main', '[role="main"]', '.content', '.post-content', '.entry-content', '.article-content', '#content', '.main-content', ); $main_content = ''; foreach ( $content_selectors as $selector ) { $elements = $xpath->query( "//*[contains(@class, '" . str_replace( array( '[', ']', '.', '#' ), '', $selector ) . "')]" ); if ( $elements->length > 0 ) { foreach ( $elements as $element ) { $main_content .= $element->textContent . ' '; } break; } } // If no main content found, extract from body but exclude common noise if ( empty( trim( $main_content ) ) ) { $plain_text = wp_strip_all_tags( $html_content ); // Remove common noise patterns $plain_text = preg_replace( '/\b(cookie|privacy|policy|terms|conditions|javascript|advertisement|ads)\b/i', '', $plain_text ); } else { $plain_text = $main_content; } // Clean up whitespace $plain_text = preg_replace( '/\s+/', ' ', trim( $plain_text ) ); // Limit content length to prevent massive entries if ( strlen( $plain_text ) > 15000 ) { $plain_text = substr( $plain_text, 0, 15000 ) . '... [Content truncated]'; } return array( 'title' => $title, 'content' => $plain_text, 'description' => $description, 'author' => $author, 'published_date' => $published_date, 'keywords' => $keywords, ); } private function summarize_new_context( $url, $user_id ) { $summary_prompt = "You have just successfully read the content from the URL: {$url}. Now, provide a concise summary of its key points based on the context you've just learned."; return $this->query( $summary_prompt, 'private', $user_id ); } public function find_relevant_context( $query_text, $limit = 5 ) { global $wpdb; // Create cache key based on query and limit $cache_key = 'aiohm_context_' . md5( $query_text . $limit ); $cached_result = wp_cache_get( $cache_key, 'aiohm_kb' ); if ( false !== $cached_result ) { return $cached_result; } $ai_client = new AIOHM_KB_AI_GPT_Client(); $query_embedding = $ai_client->generate_embeddings( $query_text ); $keywords = preg_replace( '/[^a-z0-9\s]/i', '', strtolower( $query_text ) ); $table_name = $this->table_name; if ( ! empty( trim( $keywords ) ) ) { $search_query = '+' . str_replace( ' ', ' +', trim( $keywords ) ); $cache_key_search = 'aiohm_search_' . md5( $search_query ); $pre_filtered_entries = wp_cache_get( $cache_key_search, 'aiohm_kb' ); if ( false === $pre_filtered_entries ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Vector search operations require direct database access with caching implementation for performance // Validate table name for security $safe_table = $this->get_safe_table_name( $wpdb->prefix . 'aiohm_vector_entries' ); $pre_filtered_entries = $wpdb->get_results( $wpdb->prepare( 'SELECT id, title, content, content_type, metadata, vector_data FROM `' . esc_sql( $safe_table ) . '` WHERE user_id = 0 AND MATCH(content) AGAINST(%s IN BOOLEAN MODE)', $search_query ), ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_cache_set( $cache_key_search, $pre_filtered_entries, 'aiohm_kb', 300 ); } } else { $cache_key_all = 'aiohm_all_entries'; $pre_filtered_entries = wp_cache_get( $cache_key_all, 'aiohm_kb' ); if ( false === $pre_filtered_entries ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // All entries query with caching implementation for vector search performance // Validate table name for security $safe_table = $this->get_safe_table_name( $wpdb->prefix . 'aiohm_vector_entries' ); $pre_filtered_entries = $wpdb->get_results( 'SELECT id, title, content, content_type, metadata, vector_data FROM `' . esc_sql( $safe_table ) . '` WHERE user_id = 0', ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_cache_set( $cache_key_all, $pre_filtered_entries, 'aiohm_kb', 600 ); } } if ( empty( $pre_filtered_entries ) ) { $cache_key_random = 'aiohm_random_entries'; $pre_filtered_entries = wp_cache_get( $cache_key_random, 'aiohm_kb' ); if ( false === $pre_filtered_entries ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Random sampling operations require direct database access for efficient offset-based selection with caching // Validate table name for security $safe_table = $this->get_safe_table_name( $wpdb->prefix . 'aiohm_vector_entries' ); // Efficient random sampling using offset-based selection instead of ORDER BY RAND() // First get the total count for calculating random offsets // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Safe table name is validated via get_safe_table_name method $total_count = $wpdb->get_var( "SELECT COUNT(*) FROM `$safe_table` WHERE user_id = 0" ); $random_entries = array(); if ( $total_count > 0 ) { $sample_size = min( 100, $total_count ); $offsets = array(); // Generate unique random offsets while ( count( $offsets ) < $sample_size ) { $offset = wp_rand( 0, $total_count - 1 ); if ( ! in_array( $offset, $offsets ) ) { $offsets[] = $offset; } } // Fetch entries at random offsets foreach ( $offsets as $offset ) { $entry = $wpdb->get_row( $wpdb->prepare( 'SELECT id, title, content, content_type, metadata, vector_data FROM `' . esc_sql( $safe_table ) . '` WHERE user_id = 0 LIMIT 1 OFFSET %d', $offset ), ARRAY_A ); if ( $entry ) { $random_entries[] = $entry; } } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching } $pre_filtered_entries = $random_entries; wp_cache_set( $cache_key_random, $pre_filtered_entries, 'aiohm_kb', 180 ); // 3 minute cache for random results } } $similarities = array(); foreach ( $pre_filtered_entries as $entry ) { $vector = json_decode( $entry['vector_data'], true ); if ( is_array( $vector ) ) { $dot_product = array_sum( array_map( fn( $a, $b ) => $a * $b, $query_embedding, $vector ) ); $mag_a = sqrt( array_sum( array_map( fn( $a ) => $a * $a, $query_embedding ) ) ); $mag_b = sqrt( array_sum( array_map( fn( $b ) => $b * $b, $vector ) ) ); if ( $mag_a > 0 && $mag_b > 0 ) { $similarities[] = array( 'score' => $dot_product / ( $mag_a * $mag_b ), 'entry' => $entry, ); } } } usort( $similarities, fn( $a, $b ) => $b['score'] <=> $a['score'] ); $result = array_slice( $similarities, 0, $limit ); // Cache the final result wp_cache_set( $cache_key, $result, 'aiohm_kb', 300 ); return $result; } public function get_all_entries_paginated( $per_page = 20, $page_number = 1 ) { global $wpdb; $cache_key = 'aiohm_entries_page_' . $page_number . '_' . $per_page; $cached_result = wp_cache_get( $cache_key, 'aiohm_kb' ); if ( false !== $cached_result ) { return $cached_result; } $offset = ( $page_number - 1 ) * $per_page; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Export query with caching implementation for paginated data retrieval // Validate table name for security $safe_table = $this->get_safe_table_name( $wpdb->prefix . 'aiohm_vector_entries' ); $result = $wpdb->get_results( $wpdb->prepare( 'SELECT id, title, content_type, user_id, content_id, metadata, created_at FROM `' . esc_sql( $safe_table ) . '` GROUP BY content_id ORDER BY id DESC LIMIT %d OFFSET %d', $per_page, $offset ), ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_cache_set( $cache_key, $result, 'aiohm_kb', 600 ); return $result; } public function get_total_entries_count() { global $wpdb; $cache_key = 'aiohm_total_entries_count'; $cached_count = wp_cache_get( $cache_key, 'aiohm_kb' ); if ( false !== $cached_count ) { return (int) $cached_count; } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Count query with caching implementation for total entries tracking $safe_table_name = $wpdb->prefix . 'aiohm_vector_entries'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name uses WordPress prefix, considered safe $count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT content_id) FROM `$safe_table_name`" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_cache_set( $cache_key, $count, 'aiohm_kb', 3600 ); // Cache for 1 hour return $count; } public function delete_entry_by_content_id( $content_id ) { global $wpdb; $table_name = $this->table_name; // Get metadata before deletion for cache invalidation $cache_key_meta = 'aiohm_meta_' . md5( $content_id ); $metadata_json = wp_cache_get( $cache_key_meta, 'aiohm_kb' ); if ( false === $metadata_json ) { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Metadata lookup and delete operations require direct database access with proper cache invalidation // Validate table name for security $safe_table = $this->get_safe_table_name( $wpdb->prefix . 'aiohm_vector_entries' ); $metadata_json = $wpdb->get_var( $wpdb->prepare( 'SELECT metadata FROM `' . esc_sql( $safe_table ) . '` WHERE content_id = %s LIMIT 1', $content_id ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching wp_cache_set( $cache_key_meta, $metadata_json, 'aiohm_kb', 300 ); } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery // Delete operation with cache invalidation requires direct database access $deleted = $wpdb->delete( $this->table_name, array( 'content_id' => $content_id ), array( '%s' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery if ( $deleted > 0 ) { // Invalidate relevant caches $this->clear_search_caches(); wp_cache_delete( $cache_key_meta, 'aiohm_kb' ); wp_cache_delete( 'aiohm_total_entries_count', 'aiohm_kb' ); if ( ! empty( $metadata_json ) ) { $metadata = json_decode( $metadata_json, true ); $original_item_id = null; if ( isset( $metadata['post_id'] ) ) { $original_item_id = (int) $metadata['post_id']; } elseif ( isset( $metadata['attachment_id'] ) ) { $original_item_id = (int) $metadata['attachment_id']; } if ( $original_item_id ) { delete_post_meta( $original_item_id, '_aiohm_indexed' ); clean_post_cache( $original_item_id ); } } } return $deleted; } private function clear_search_caches() { // Clear all search-related caches wp_cache_flush_group( 'aiohm_kb' ); } private function generate_entry_id( $title, $content ) { // Include timestamp and user ID to make content_id more unique $user_id = get_current_user_id(); $timestamp = current_time( 'timestamp' ); return md5( $title . $content . $user_id . $timestamp ); } private function chunk_content( $content, $chunk_size, $chunk_overlap ) { $content = trim( $content ); if ( empty( $content ) ) { return array(); } if ( strlen( $content ) <= $chunk_size ) { return array( $content ); } // Use intelligent chunking that respects sentence boundaries return $this->intelligent_chunk_content( $content, $chunk_size, $chunk_overlap ); } /** * Intelligent content chunking that respects sentence and paragraph boundaries * * @param string $content Content to chunk * @param int $chunk_size Target chunk size * @param int $chunk_overlap Overlap between chunks * @return array Array of content chunks */ private function intelligent_chunk_content( $content, $chunk_size, $chunk_overlap ) { $chunks = array(); $paragraphs = preg_split( '/\n\s*\n/', $content ); $current_chunk = ''; foreach ( $paragraphs as $paragraph ) { $paragraph = trim( $paragraph ); if ( empty( $paragraph ) ) { continue; } if ( ! empty( $current_chunk ) && ( strlen( $current_chunk ) + strlen( $paragraph ) + 2 ) > $chunk_size ) { $processed_chunks = $this->process_chunk_with_sentences( $current_chunk, $chunk_size, $chunk_overlap ); $chunks = array_merge( $chunks, $processed_chunks ); $current_chunk = $this->get_chunk_overlap( $chunks, $chunk_overlap ) . $paragraph; } else { $current_chunk .= ( empty( $current_chunk ) ? '' : "\n\n" ) . $paragraph; } } if ( ! empty( $current_chunk ) ) { $processed_chunks = $this->process_chunk_with_sentences( $current_chunk, $chunk_size, $chunk_overlap ); $chunks = array_merge( $chunks, $processed_chunks ); } if ( empty( $chunks ) ) { return $this->basic_chunk_content( $content, $chunk_size, $chunk_overlap ); } return array_filter( $chunks, function ( $chunk ) { return ! empty( trim( $chunk ) ); } ); } /** * Process a chunk with sentence-boundary awareness * * @param string $content Content to process * @param int $chunk_size Target chunk size * @param int $chunk_overlap Overlap size * @return array Array of processed chunks */ private function process_chunk_with_sentences( $content, $chunk_size, $chunk_overlap ) { if ( strlen( $content ) <= $chunk_size ) { return array( $content ); } $chunks = array(); $sentences = preg_split( '/(?<=[.!?])\s+(?=[A-Z])|(?<=[.!?])\n+/', $content, -1, PREG_SPLIT_NO_EMPTY ); if ( empty( $sentences ) ) { return $this->basic_chunk_content( $content, $chunk_size, $chunk_overlap ); } $current_chunk = ''; $sentence_buffer = array(); foreach ( $sentences as $sentence ) { $sentence = trim( $sentence ); if ( empty( $sentence ) ) { continue; } $potential_chunk = $current_chunk . ( empty( $current_chunk ) ? '' : ' ' ) . $sentence; if ( strlen( $potential_chunk ) <= $chunk_size ) { $current_chunk = $potential_chunk; $sentence_buffer[] = $sentence; } else { if ( ! empty( $current_chunk ) ) { $chunks[] = $current_chunk; } if ( strlen( $sentence ) > $chunk_size ) { $long_sentence_chunks = $this->split_long_sentence( $sentence, $chunk_size ); $chunks = array_merge( $chunks, $long_sentence_chunks ); $current_chunk = ''; $sentence_buffer = array(); } else { $overlap_text = $this->get_sentence_overlap( $sentence_buffer, $chunk_overlap ); $current_chunk = $overlap_text . ( empty( $overlap_text ) ? '' : ' ' ) . $sentence; $sentence_buffer = $overlap_text ? array( end( $sentence_buffer ), $sentence ) : array( $sentence ); } } } if ( ! empty( $current_chunk ) ) { $chunks[] = $current_chunk; } return $chunks; } /** * Split a very long sentence that exceeds chunk size * * @param string $sentence Long sentence to split * @param int $chunk_size Maximum chunk size * @return array Array of sentence fragments */ private function split_long_sentence( $sentence, $chunk_size ) { $chunks = array(); $parts = preg_split( '/([,;:])\s*/', $sentence, -1, PREG_SPLIT_DELIM_CAPTURE ); $current_chunk = ''; for ( $i = 0; $i < count( $parts ); $i += 2 ) { $part = $parts[ $i ]; $delimiter = isset( $parts[ $i + 1 ] ) ? $parts[ $i + 1 ] : ''; $potential_chunk = $current_chunk . $part . $delimiter; if ( strlen( $potential_chunk ) <= $chunk_size ) { $current_chunk = $potential_chunk; } else { if ( ! empty( $current_chunk ) ) { $chunks[] = trim( $current_chunk ); } if ( strlen( $part . $delimiter ) > $chunk_size ) { $char_chunks = $this->basic_chunk_content( $part . $delimiter, $chunk_size, 50 ); $chunks = array_merge( $chunks, $char_chunks ); $current_chunk = ''; } else { $current_chunk = $part . $delimiter; } } } if ( ! empty( $current_chunk ) ) { $chunks[] = trim( $current_chunk ); } return array_filter( $chunks, function ( $chunk ) { return ! empty( trim( $chunk ) ); } ); } /** * Get overlap text from previous chunks * * @param array $chunks Previous chunks * @param int $overlap_size Target overlap size * @return string Overlap text */ private function get_chunk_overlap( $chunks, $overlap_size ) { if ( empty( $chunks ) || $overlap_size <= 0 ) { return ''; } $last_chunk = end( $chunks ); if ( strlen( $last_chunk ) <= $overlap_size ) { return $last_chunk; } $overlap_text = substr( $last_chunk, -$overlap_size ); $sentence_start = strpos( $overlap_text, '. ' ); if ( $sentence_start !== false && $sentence_start < $overlap_size / 2 ) { return substr( $overlap_text, $sentence_start + 2 ); } return $overlap_text; } /** * Get sentence-based overlap from sentence buffer * * @param array $sentence_buffer Buffer of recent sentences * @param int $overlap_size Target overlap size * @return string Overlap text */ private function get_sentence_overlap( $sentence_buffer, $overlap_size ) { if ( empty( $sentence_buffer ) || $overlap_size <= 0 ) { return ''; } $overlap = ''; for ( $i = count( $sentence_buffer ) - 1; $i >= 0; $i-- ) { $potential_overlap = $sentence_buffer[ $i ] . ( empty( $overlap ) ? '' : ' ' . $overlap ); if ( strlen( $potential_overlap ) <= $overlap_size ) { $overlap = $potential_overlap; } else { break; } } return $overlap; } /** * Basic character-based chunking as fallback * * @param string $content Content to chunk * @param int $chunk_size Chunk size * @param int $chunk_overlap Overlap size * @return array Array of chunks */ private function basic_chunk_content( $content, $chunk_size, $chunk_overlap ) { $chunks = array(); $content_length = strlen( $content ); $start = 0; while ( $start < $content_length ) { $chunk = substr( $content, $start, $chunk_size ); $chunks[] = $chunk; $start += ( $chunk_size - $chunk_overlap ); } return $chunks; } public function export_knowledge_base( $user_id = 0 ) { global $wpdb; try { // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Knowledge base export operations require direct database access with table validation for data integrity // Check if table exists $table_name = $wpdb->prefix . 'aiohm_vector_entries'; $table_check = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ); if ( ! $table_check ) { throw new Exception( 'Vector entries table does not exist' ); } // Validate table name for security $safe_table = $this->get_safe_table_name( $wpdb->prefix . 'aiohm_vector_entries' ); // Get data with specific columns to avoid memory issues // Filter by both user_id and is_public to ensure correct scope separation if ( $user_id === 0 ) { // Public export: user_id = 0 AND is_public = 1 $data = $wpdb->get_results( $wpdb->prepare( 'SELECT id, user_id, content_id, content_type, title, content, metadata, created_at FROM `' . esc_sql( $safe_table ) . '` WHERE user_id = %d AND is_public = 1', $user_id ), ARRAY_A ); } else { // Private export: user_id = current_user AND is_public = 0 $data = $wpdb->get_results( $wpdb->prepare( 'SELECT id, user_id, content_id, content_type, title, content, metadata, created_at FROM `' . esc_sql( $safe_table ) . '` WHERE user_id = %d AND is_public = 0', $user_id ), ARRAY_A ); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching if ( $wpdb->last_error ) { // Return generic error to user, log detailed error internally throw new Exception( 'Failed to export knowledge base. Please try again.' ); } // Return empty array as JSON if no data found if ( empty( $data ) ) { return json_encode( array(), JSON_PRETTY_PRINT ); } // Try encoding without vector_data first for public exports if ( $user_id === 0 ) { // Remove large vector_data for public exports to avoid memory issues foreach ( $data as &$entry ) { unset( $entry['vector_data'] ); } } $json_result = json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); if ( json_last_error() !== JSON_ERROR_NONE ) { throw new Exception( 'JSON encoding error: ' . json_last_error_msg() ); } return $json_result; } catch ( Exception $e ) { throw $e; } } public function import_knowledge_base( $json_data ) { global $wpdb; $data = json_decode( $json_data, true ); if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) { throw new Exception( 'Invalid JSON data provided.' ); } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Bulk import operations require direct database access with transaction management for data integrity $wpdb->query( 'START TRANSACTION' ); try { // Bulk delete with cache clearing $wpdb->delete( $this->table_name, array( 'user_id' => 0 ), array( '%d' ) ); foreach ( $data as $row ) { if ( isset( $row['content_id'], $row['content_type'], $row['title'], $row['content'] ) ) { $wpdb->insert( $this->table_name, array( 'user_id' => 0, 'content_id' => $row['content_id'], 'content_type' => $row['content_type'], 'title' => $row['title'], 'content' => $row['content'], 'vector_data' => $row['vector_data'] ?? '[]', 'metadata' => $row['metadata'] ?? '[]', ), array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', ) ); } } // Transaction commit with cache clearing $wpdb->query( 'COMMIT' ); // Clear all caches after successful import $this->clear_search_caches(); } catch ( Exception $e ) { // Transaction rollback required $wpdb->query( 'ROLLBACK' ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching throw $e; } return count( $data ); } public function find_context_for_user( $query_text, $user_id, $limit = 5 ) { global $wpdb; $ai_client = new AIOHM_KB_AI_GPT_Client(); $query_embedding = $ai_client->generate_embeddings( $query_text ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // User context search query requires direct database access for vector similarity operations // Validate table name for security $safe_table = $this->get_safe_table_name( $wpdb->prefix . 'aiohm_vector_entries' ); $all_entries = $wpdb->get_results( $wpdb->prepare( 'SELECT id, title, content, content_type, metadata, vector_data FROM `' . esc_sql( $safe_table ) . '` WHERE user_id = 0 OR user_id = %d', $user_id ), ARRAY_A ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $similarities = array(); foreach ( $all_entries as $entry ) { $vector = ! empty( $entry['vector_data'] ) ? json_decode( $entry['vector_data'], true ) : null; if ( is_array( $vector ) ) { $dot_product = array_sum( array_map( fn( $a, $b ) => $a * $b, $query_embedding, $vector ) ); $mag_a = sqrt( array_sum( array_map( fn( $a ) => $a * $a, $query_embedding ) ) ); $mag_b = sqrt( array_sum( array_map( fn( $b ) => $b * $b, $vector ) ) ); if ( $mag_a > 0 && $mag_b > 0 ) { $similarities[] = array( 'score' => $dot_product / ( $mag_a * $mag_b ), 'entry' => $entry, ); } } } usort( $similarities, fn( $a, $b ) => $b['score'] <=> $a['score'] ); return array_slice( $similarities, 0, $limit ); } public function update_entry_scope_by_content_id( $content_id, $new_user_id ) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching // Direct update with cache clearing required for scope management operations $result = $wpdb->update( $this->table_name, array( 'user_id' => $new_user_id ), array( 'content_id' => $content_id ), array( '%d' ), array( '%s' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching if ( $result ) { // Clear relevant caches after update $this->clear_search_caches(); } return $result; } public function update_entry_visibility_by_content_id( $content_id, $is_public ) { global $wpdb; $update_data = array( 'is_public' => $is_public ); $update_formats = array( '%d' ); if ( $is_public ) { // Public: user_id = 0, is_public = 1 $update_data['user_id'] = 0; $update_formats[] = '%d'; } else { // Private: user_id = current_user_id, is_public = 0 $update_data['user_id'] = get_current_user_id(); $update_formats[] = '%d'; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update with cache clearing $result = $wpdb->update( $this->table_name, $update_data, array( 'content_id' => $content_id ), $update_formats, array( '%s' ) ); if ( $result ) { // Clear relevant caches after update $this->clear_search_caches(); } return $result; } public function get_random_chunk() { global $wpdb; $table_name = $wpdb->prefix . 'aiohm_vector_entries'; // Use efficient random sampling instead of ORDER BY RAND() // First get the total count // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Count query for random sampling $total_count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM ' . $wpdb->prefix . 'aiohm_vector_entries WHERE is_public = %d', 1 ) ); if ( $total_count > 0 ) { $random_offset = wp_rand( 0, $total_count - 1 ); // Get random entry using offset // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Optimized random content query $random_entry = $wpdb->get_var( $wpdb->prepare( 'SELECT content FROM ' . $wpdb->prefix . 'aiohm_vector_entries WHERE is_public = %d LIMIT 1 OFFSET %d', 1, $random_offset ) ); return $random_entry; } return null; } }