PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.5
MxChat – AI Chatbot & Content Generation for WordPress v3.1.5
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
mxchat-basic / includes / class-mxchat-utils.php

class-mxchat-utils.php in MxChat – AI Chatbot & Content Generation for WordPress 3.1.5, at includes/class-mxchat-utils.php

900 lines 35.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit; // Exit if accessed directly
4 }
5
6 class MxChat_Utils {
7
8 /**
9 * UPDATED: Submit or update content (and its embedding) in the database.
10 * Stores in Pinecone if enabled, otherwise stores in WordPress DB.
11 *
12 * @param string $content The content to be embedded.
13 * @param string $source_url The source URL of the content.
14 * @param string $api_key The API key used for generating embeddings.
15 * @param string $vector_id Optional vector ID for Pinecone (if not provided, will use md5 of URL)
16 * @param string $bot_id The bot ID for multi-bot support
17 * @param string $content_type The type of content (post, page, pdf, url, manual, product, etc.)
18 * @return bool|WP_Error True on success, WP_Error on failure
19 */
20 public static function submit_content_to_db($content, $source_url, $api_key, $vector_id = null, $bot_id = 'default', $content_type = 'content') {
21 global $wpdb;
22 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
23
24 //error_log('[MXCHAT-DB] Starting database submission for URL: ' . $source_url . ' (Bot: ' . $bot_id . ', Type: ' . $content_type . ')');
25 //error_log('[MXCHAT-DB] Content length: ' . strlen($content) . ' bytes');
26
27 // Sanitize the source URL
28 $source_url = esc_url_raw($source_url);
29
30 // Sanitize content_type
31 $content_type = sanitize_key($content_type);
32 if (empty($content_type)) {
33 $content_type = 'content'; // Fallback for backwards compatibility
34 }
35
36 // Just ensure UTF-8 validity without aggressive escaping
37 $safe_content = wp_check_invalid_utf8($content);
38 // Remove only null bytes and other control characters, but preserve newlines (\n = \x0A) and carriage returns (\r = \x0D)
39 $safe_content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $safe_content);
40
41 // Check if chunking should be applied
42 $chunker = MxChat_Chunker::from_settings();
43 if ($chunker->should_chunk($safe_content)) {
44 //error_log('[MXCHAT-DB] Content exceeds chunk threshold, using chunked submission');
45 return self::submit_chunked_content($safe_content, $source_url, $api_key, $bot_id, $content_type, $chunker);
46 }
47
48 // UPDATED: Generate the embedding using bot-specific configuration
49 $embedding_vector = self::generate_embedding($content, $api_key, $bot_id);
50
51 if (!is_array($embedding_vector)) {
52 //error_log('[MXCHAT-DB] Error: Embedding generation failed');
53 return new WP_Error('embedding_failed', 'Failed to generate embedding for content');
54 }
55
56 //error_log('[MXCHAT-DB] Embedding generated successfully');
57
58 // UPDATED: Check if Pinecone is enabled for this specific bot
59 if (self::is_pinecone_enabled_for_bot($bot_id)) {
60 //error_log('[MXCHAT-DB] Pinecone is enabled for bot ' . $bot_id . ' - using Pinecone storage');
61 // Store in Pinecone only
62 return self::store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id, $bot_id, $content_type);
63 } else {
64 //error_log('[MXCHAT-DB] Pinecone not enabled for bot ' . $bot_id . ' - using WordPress storage');
65 // Store in WordPress database only
66 $embedding_vector_serialized = maybe_serialize($embedding_vector);
67 return self::store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name, $content_type);
68 }
69 }
70
71 /**
72 * UPDATED: Check if Pinecone is enabled and properly configured for a specific bot
73 */
74 private static function is_pinecone_enabled_for_bot($bot_id = 'default') {
75 // For default bot or when multi-bot is not active, use original method
76 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
77 return self::is_pinecone_enabled();
78 }
79
80 // Get bot-specific Pinecone configuration
81 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
82
83 if (empty($bot_pinecone_config)) {
84 // Fallback to default configuration
85 return self::is_pinecone_enabled();
86 }
87
88 $enabled_check = !empty($bot_pinecone_config['use_pinecone']) && $bot_pinecone_config['use_pinecone'];
89 $api_key_check = !empty($bot_pinecone_config['api_key']);
90 $host_check = !empty($bot_pinecone_config['host']);
91
92 return $enabled_check && $api_key_check && $host_check;
93 }
94
95 /**
96 * Check if Pinecone is enabled and properly configured (original method for default bot)
97 */
98 private static function is_pinecone_enabled() {
99 $pinecone_options = get_option('mxchat_pinecone_addon_options');
100
101 if (empty($pinecone_options)) {
102 return false;
103 }
104
105 $enabled_check = !empty($pinecone_options['mxchat_use_pinecone']) && $pinecone_options['mxchat_use_pinecone'] !== '0';
106 $api_key_check = !empty($pinecone_options['mxchat_pinecone_api_key']);
107 $host_check = !empty($pinecone_options['mxchat_pinecone_host']);
108
109 return $enabled_check && $api_key_check && $host_check;
110 }
111
112 /**
113 * UPDATED: Store content in Pinecone only with bot support
114 */
115 private static function store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id = null, $bot_id = 'default', $content_type = 'content') {
116 //error_log('[MXCHAT-PINECONE] ===== Using Pinecone-only storage for bot ' . $bot_id . ' =====');
117
118 // Get bot-specific Pinecone configuration
119 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
120 $pinecone_options = get_option('mxchat_pinecone_addon_options');
121 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
122 $environment = $pinecone_options['mxchat_pinecone_environment'] ?? '';
123 $index_name = $pinecone_options['mxchat_pinecone_index'] ?? '';
124 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
125 } else {
126 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
127 if (empty($bot_pinecone_config)) {
128 // Fallback to default configuration
129 $pinecone_options = get_option('mxchat_pinecone_addon_options');
130 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
131 $environment = $pinecone_options['mxchat_pinecone_environment'] ?? '';
132 $index_name = $pinecone_options['mxchat_pinecone_index'] ?? '';
133 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
134 } else {
135 $api_key = $bot_pinecone_config['api_key'];
136 $environment = ''; // Not used in new Pinecone API
137 $index_name = ''; // Not used in new Pinecone API
138 $namespace = $bot_pinecone_config['namespace'] ?? '';
139 }
140 }
141
142 $result = self::store_in_pinecone_main(
143 $embedding_vector,
144 $content,
145 $source_url,
146 $api_key,
147 $environment,
148 $index_name,
149 $vector_id,
150 $bot_id,
151 $namespace,
152 $content_type
153 );
154
155 if (is_wp_error($result)) {
156 //error_log('[MXCHAT-PINECONE] Pinecone storage failed for bot ' . $bot_id . ': ' . $result->get_error_message());
157 return $result;
158 }
159
160 //error_log('[MXCHAT-PINECONE] Pinecone storage completed successfully for bot ' . $bot_id);
161 return true;
162 }
163
164 /**
165 * Store content in WordPress database with progressive fallback
166 * UPDATED 2.5.6: Now includes content_type parameter
167 */
168 private static function store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name, $content_type = 'content') {
169 global $wpdb;
170
171 //error_log('[MXCHAT-DB] ===== Using WordPress-only storage =====');
172
173 // Sanitize content_type
174 $content_type = sanitize_key($content_type);
175 if (empty($content_type)) {
176 $content_type = 'content'; // Fallback for backwards compatibility
177 }
178
179 // ===== FIXED: Generate unique identifier for manual content =====
180 $original_source_url = $source_url;
181 // Check if this is truly manual content (no URL at all) vs a real URL that filter_var rejects
182 // filter_var(FILTER_VALIDATE_URL) rejects valid URLs with encoded chars, non-ASCII, fragments, etc.
183 // Use a looser check: if it starts with http(s):// or has a scheme, it's a URL
184 $has_url_scheme = !empty($source_url) && preg_match('#^https?://#i', $source_url);
185 $is_manual_content = empty($source_url) || $source_url === '' || !$has_url_scheme;
186
187 if ($is_manual_content) {
188 // Generate unique identifier for manual content to prevent overwrites
189 $source_url = 'mxchat://manual-content/' . time() . '-' . wp_generate_password(8, false);
190 //error_log('[MXCHAT-DB] Generated unique ID for manual content: ' . $source_url);
191 }
192
193 // Only check for duplicates if we have a valid source URL (not manual content)
194 $existing_id = null;
195 if (!$is_manual_content) {
196 $existing_id = $wpdb->get_var(
197 $wpdb->prepare(
198 "SELECT id FROM {$table_name} WHERE source_url = %s LIMIT 1",
199 $source_url
200 )
201 );
202 //error_log('[MXCHAT-DB] Checked for existing URL, found ID: ' . ($existing_id ?: 'none'));
203 } else {
204 //error_log('[MXCHAT-DB] Manual content - will create new entry (no duplicate check)');
205 }
206 // ===== END FIX =====
207
208 // Progressive fallback mechanism for problematic content
209 $attempt = 1;
210 $max_attempts = 3;
211 $current_content = $safe_content;
212 $result = false;
213
214 while ($attempt <= $max_attempts && $result === false) {
215 try {
216 if ($existing_id) {
217 //error_log('[MXCHAT-DB] Found existing entry (ID: ' . $existing_id . '). Updating... (Attempt ' . $attempt . ')');
218
219 // Update the existing row - UPDATED 2.5.6: Added content_type
220 $result = $wpdb->update(
221 $table_name,
222 array(
223 'url' => $source_url,
224 'article_content' => $current_content,
225 'embedding_vector' => $embedding_vector_serialized,
226 'source_url' => $source_url,
227 'content_type' => $content_type,
228 'timestamp' => current_time('mysql'),
229 ),
230 array('id' => $existing_id),
231 array('%s','%s','%s','%s','%s','%s'),
232 array('%d')
233 );
234 } else {
235 //error_log('[MXCHAT-DB] No existing entry found. Inserting new row... (Attempt ' . $attempt . ')');
236 //error_log('[MXCHAT-DB] Content sample: ' . substr($current_content, 0, 1000));
237
238 // Insert a new row - UPDATED 2.5.6: Added content_type
239 $result = $wpdb->insert(
240 $table_name,
241 array(
242 'url' => $source_url, // Now unique for manual content
243 'article_content' => $current_content,
244 'embedding_vector' => $embedding_vector_serialized,
245 'source_url' => $source_url, // Now unique for manual content
246 'content_type' => $content_type,
247 'timestamp' => current_time('mysql'),
248 ),
249 array('%s','%s','%s','%s','%s','%s')
250 );
251 }
252
253 if ($result === false) {
254 //error_log('[MXCHAT-DB] Database operation failed (Attempt ' . $attempt . ')');
255 //error_log('[MXCHAT-DB] MySQL Error: ' . $wpdb->last_error);
256 //error_log('[MXCHAT-DB] MySQL Error Number: ' . $wpdb->last_errno);
257 //error_log('[MXCHAT-DB] Last Query: ' . substr($wpdb->last_query, 0, 500));
258 //error_log('[MXCHAT-DB] Content length: ' . strlen($current_content) . ' bytes');
259 //error_log('[MXCHAT-DB] Embedding vector length: ' . strlen($embedding_vector_serialized) . ' bytes');
260
261 // Progressively apply more aggressive sanitization on failure
262 if ($attempt === 1) {
263 // First fallback: Use a more aggressive character filter and shorten
264 $current_content = preg_replace('/[^\p{L}\p{N}\s.,;:!?()-]/u', '', $current_content);
265 $current_content = substr($current_content, 0, 50000);
266 } else if ($attempt === 2) {
267 // Second fallback: Keep only alphanumeric and basic punctuation, shorten further
268 $current_content = preg_replace('/[^a-zA-Z0-9\s.,;:!?()-]/u', '', $current_content);
269 $current_content = substr($current_content, 0, 30000);
270 }
271
272 $attempt++;
273 }
274 } catch (Exception $e) {
275 //error_log('[MXCHAT-DB] Exception during database operation: ' . $e->getMessage());
276 $attempt++;
277 }
278 }
279
280 if ($result === false) {
281 //error_log('[MXCHAT-DB] All database operation attempts failed');
282 //error_log('[MXCHAT-DB] Final MySQL Error: ' . $wpdb->last_error);
283
284 $detailed_error = sprintf(
285 'Failed to store content in WordPress database after %d attempts. MySQL Error: %s (Error #%d). Content size: %d bytes, Embedding size: %d bytes',
286 $max_attempts,
287 $wpdb->last_error,
288 $wpdb->last_errno,
289 strlen($current_content),
290 strlen($embedding_vector_serialized)
291 );
292
293 return new WP_Error('database_failed', $detailed_error);
294 }
295
296 //error_log('[MXCHAT-DB] WordPress database operation completed successfully (Attempt ' . ($attempt - 1) . ')');
297 return true;
298 }
299
300 /**
301 * UPDATED: Store content in Pinecone database with bot support
302 * UPDATED 2.5.6: Now accepts content_type parameter
303 */
304 private static function store_in_pinecone_main($embedding_vector, $content, $url, $api_key, $environment, $index_name, $vector_id = null, $bot_id = 'default', $namespace = '', $content_type = 'content') {
305 //error_log('[MXCHAT-PINECONE-MAIN] ===== Starting Pinecone storage for bot ' . $bot_id . ' =====');
306
307 // ===== UPDATED: Handle manual content with unique vector IDs =====
308 if ($vector_id) {
309 // Use provided vector ID
310 //error_log('[MXCHAT-PINECONE-MAIN] Using provided vector ID: ' . $vector_id);
311 } elseif (!empty($url) && preg_match('#^https?://#i', $url)) {
312 // For URLs, use URL-based ID (existing behavior)
313 $vector_id = md5($url);
314 //error_log('[MXCHAT-PINECONE-MAIN] Generated vector ID from URL: ' . $vector_id);
315 } else {
316 // For manual content (empty/no URL scheme), generate unique ID
317 $vector_id = 'manual_' . time() . '_' . substr(md5($content . microtime(true)), 0, 8);
318 //error_log('[MXCHAT-PINECONE-MAIN] Generated unique vector ID for manual content: ' . $vector_id);
319 }
320 // ===== END UPDATE =====
321
322 // Get host from bot-specific config or fallback to default
323 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
324 $options = get_option('mxchat_pinecone_addon_options');
325 $host = $options['mxchat_pinecone_host'] ?? '';
326 } else {
327 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
328 if (!empty($bot_pinecone_config)) {
329 $host = $bot_pinecone_config['host'] ?? '';
330 } else {
331 $options = get_option('mxchat_pinecone_addon_options');
332 $host = $options['mxchat_pinecone_host'] ?? '';
333 }
334 }
335
336 //error_log('[MXCHAT-PINECONE-MAIN] Host: ' . $host);
337 //error_log('[MXCHAT-PINECONE-MAIN] API key length: ' . strlen($api_key));
338 //error_log('[MXCHAT-PINECONE-MAIN] Bot ID: ' . $bot_id);
339 //error_log('[MXCHAT-PINECONE-MAIN] Namespace: ' . $namespace);
340
341 if (empty($host)) {
342 //error_log('[MXCHAT-PINECONE-MAIN] ERROR: Host is empty');
343 return new WP_Error('pinecone_config', 'Pinecone host is not configured. Please set the host in your bot settings.');
344 }
345
346 // ===== UPDATED 2.5.6: Use passed content_type or determine from URL if not provided =====
347 // Sanitize content_type
348 $content_type = sanitize_key($content_type);
349 if (empty($content_type)) {
350 // Fallback to old detection logic for backwards compatibility
351 $is_product = false;
352 $content_type = 'manual'; // Default for manual content
353
354 if (!empty($url) && preg_match('#^https?://#i', $url)) {
355 $is_product = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
356 $content_type = $is_product ? 'product' : 'content';
357 }
358 }
359
360 //error_log('[MXCHAT-PINECONE-MAIN] Content type: ' . $content_type);
361 // ===== END UPDATE =====
362
363 $api_endpoint = "https://{$host}/vectors/upsert";
364 //error_log('[MXCHAT-PINECONE-MAIN] API endpoint: ' . $api_endpoint);
365
366 // UPDATED 2.5.6: Use provided content_type in metadata
367 $metadata = array(
368 'text' => $content,
369 'source_url' => $url, // Can be empty for manual content
370 'type' => $content_type, // Now supports: post, page, pdf, url, manual, product, etc.
371 'last_updated' => time(),
372 'created_at' => time(), // Add creation timestamp
373 'bot_id' => $bot_id // Add bot identification
374 );
375
376 $vector_data = array(
377 'id' => $vector_id,
378 'values' => $embedding_vector,
379 'metadata' => $metadata
380 );
381
382 $request_body = array(
383 'vectors' => array($vector_data)
384 );
385
386 // Add namespace if specified for multi-bot separation
387 if (!empty($namespace)) {
388 $request_body['namespace'] = $namespace;
389 //error_log('[MXCHAT-PINECONE-MAIN] Using namespace: ' . $namespace);
390 }
391
392 //error_log('[MXCHAT-PINECONE-MAIN] Request body prepared (embedding dimensions: ' . count($embedding_vector) . ')');
393
394 $response = wp_remote_post($api_endpoint, array(
395 'headers' => array(
396 'Api-Key' => $api_key,
397 'accept' => 'application/json',
398 'content-type' => 'application/json'
399 ),
400 'body' => wp_json_encode($request_body),
401 'timeout' => 30,
402 'data_format' => 'body'
403 ));
404
405 if (is_wp_error($response)) {
406 //error_log('[MXCHAT-PINECONE-MAIN] WordPress request error: ' . $response->get_error_message());
407 return new WP_Error('pinecone_request', $response->get_error_message());
408 }
409
410 $response_code = wp_remote_retrieve_response_code($response);
411 //error_log('[MXCHAT-PINECONE-MAIN] Response code: ' . $response_code);
412
413 if ($response_code !== 200) {
414 $body = wp_remote_retrieve_body($response);
415 //error_log('[MXCHAT-PINECONE-MAIN] API error - Response body: ' . $body);
416 return new WP_Error('pinecone_api', sprintf(
417 'Pinecone API error (HTTP %d): %s',
418 $response_code,
419 $body
420 ));
421 }
422
423 $response_body = wp_remote_retrieve_body($response);
424 //error_log('[MXCHAT-PINECONE-MAIN] Success response: ' . $response_body);
425 //error_log('[MXCHAT-PINECONE-MAIN] Successfully stored in Pinecone for bot ' . $bot_id);
426 //error_log('[MXCHAT-PINECONE-MAIN] ===== Pinecone storage complete =====');
427
428 return true;
429 }
430
431 /**
432 * UPDATED: Generate an embedding for the given text using bot-specific configuration.
433 *
434 * @param string $text The text to be embedded.
435 * @param string $api_key The API key used for generating embeddings.
436 * @param string $bot_id The bot ID for multi-bot support
437 * @return array|null The embedding vector or null on failure.
438 */
439 private static function generate_embedding($text, $api_key, $bot_id = 'default') {
440 // Get bot-specific options
441 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
442 $options = get_option('mxchat_options');
443 } else {
444 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
445 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
446 }
447
448 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
449
450 // Determine endpoint and API key based on model
451 if (strpos($selected_model, 'voyage') === 0) {
452 $endpoint = 'https://api.voyageai.com/v1/embeddings';
453 $api_key = $options['voyage_api_key'] ?? '';
454 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
455 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
456 $api_key = $options['gemini_api_key'] ?? '';
457 } else {
458 $endpoint = 'https://api.openai.com/v1/embeddings';
459 // Use the bot-specific API key or fallback to passed API key
460 $api_key = $options['api_key'] ?? $api_key;
461 }
462
463 // Prepare request body based on provider
464 if (strpos($selected_model, 'gemini-embedding') === 0) {
465 // Gemini API format
466 $request_body = [
467 'model' => 'models/' . $selected_model,
468 'content' => [
469 'parts' => [
470 ['text' => $text]
471 ]
472 ],
473 'outputDimensionality' => 1536
474 ];
475
476 // Prepare headers for Gemini (API key as query parameter)
477 $endpoint .= '?key=' . $api_key;
478 $headers = [
479 'Content-Type' => 'application/json'
480 ];
481 } else {
482 // OpenAI/Voyage API format
483 $request_body = [
484 'input' => $text,
485 'model' => $selected_model
486 ];
487
488 // Add output_dimension for voyage-3-large
489 if ($selected_model === 'voyage-3-large') {
490 $request_body['output_dimension'] = 2048;
491 }
492
493 // Prepare headers for OpenAI/Voyage
494 $headers = [
495 'Content-Type' => 'application/json',
496 'Authorization' => 'Bearer ' . $api_key
497 ];
498 }
499
500 $args = [
501 'body' => wp_json_encode($request_body),
502 'headers' => $headers,
503 'timeout' => 60,
504 'redirection' => 5,
505 'blocking' => true,
506 'httpversion' => '1.0',
507 'sslverify' => true,
508 ];
509
510 $response = wp_remote_post($endpoint, $args);
511
512 if (is_wp_error($response)) {
513 //error_log('Error generating embedding for bot ' . $bot_id . ': ' . $response->get_error_message());
514 return null;
515 }
516
517 $response_body = json_decode(wp_remote_retrieve_body($response), true);
518
519 // Handle different response formats based on provider
520 if (strpos($selected_model, 'gemini-embedding') === 0) {
521 // Gemini API response format
522 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
523 return $response_body['embedding']['values'];
524 } else {
525 //error_log('Invalid response received from Gemini embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
526 return null;
527 }
528 } else {
529 // OpenAI/Voyage API response format
530 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
531 return $response_body['data'][0]['embedding'];
532 } else {
533 //error_log('Invalid response received from embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
534 return null;
535 }
536 }
537 }
538
539 /**
540 * Submit content as multiple chunks
541 *
542 * Splits large content into chunks, generates embeddings for each,
543 * and stores them with chunk metadata for later reassembly.
544 *
545 * @param string $content The content to chunk and store
546 * @param string $source_url The source URL
547 * @param string $api_key The API key for embeddings
548 * @param string $bot_id The bot ID
549 * @param string $content_type The content type
550 * @param MxChat_Chunker $chunker The chunker instance
551 * @return bool|WP_Error True on success, WP_Error on failure
552 */
553 private static function submit_chunked_content($content, $source_url, $api_key, $bot_id, $content_type, $chunker) {
554 global $wpdb;
555 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
556
557 //error_log('[MXCHAT-CHUNK-DEBUG] Starting chunked submission for: ' . $source_url);
558 //error_log('[MXCHAT-CHUNK-DEBUG] Content length: ' . strlen($content) . ' chars');
559
560 // First, delete any existing chunks for this URL (clean slate)
561 $delete_result = self::delete_chunks_for_url($source_url, $bot_id);
562 if (is_wp_error($delete_result)) {
563 //error_log('[MXCHAT-CHUNK-DEBUG] Warning: Failed to delete existing chunks: ' . $delete_result->get_error_message());
564 // Continue anyway - we'll overwrite with upsert
565 }
566
567 // Split content into chunks
568 $chunks = $chunker->chunk_text($content);
569 $total_chunks = count($chunks);
570
571 //error_log('[MXCHAT-CHUNK-DEBUG] Created ' . $total_chunks . ' chunks');
572 foreach ($chunks as $i => $chunk) {
573 //error_log('[MXCHAT-CHUNK-DEBUG] Chunk ' . $i . ' length: ' . strlen($chunk) . ' chars, preview: ' . substr($chunk, 0, 100));
574 }
575
576 //error_log('[MXCHAT-CHUNK] Split content into ' . $total_chunks . ' chunks');
577
578 if ($total_chunks === 0) {
579 return new WP_Error('chunking_failed', 'Content could not be split into chunks');
580 }
581
582 $errors = array();
583 $is_pinecone = self::is_pinecone_enabled_for_bot($bot_id);
584
585 foreach ($chunks as $index => $chunk_text) {
586 // Generate chunk metadata
587 $chunk_metadata = MxChat_Chunker::create_chunk_metadata($index, $total_chunks, $source_url);
588 $chunk_vector_id = MxChat_Chunker::generate_chunk_vector_id($source_url, $index);
589
590 //error_log('[MXCHAT-CHUNK] Processing chunk ' . ($index + 1) . '/' . $total_chunks . ' (ID: ' . $chunk_vector_id . ')');
591
592 // Generate embedding for this chunk
593 $embedding_vector = self::generate_embedding($chunk_text, $api_key, $bot_id);
594
595 if (!is_array($embedding_vector)) {
596 $errors[] = new WP_Error('embedding_failed', 'Failed to generate embedding for chunk ' . $index);
597 //error_log('[MXCHAT-CHUNK] Failed to generate embedding for chunk ' . $index);
598 continue;
599 }
600
601 if ($is_pinecone) {
602 // Store in Pinecone with chunk metadata
603 $result = self::store_chunk_in_pinecone(
604 $embedding_vector,
605 $chunk_text,
606 $source_url,
607 $chunk_vector_id,
608 $bot_id,
609 $content_type,
610 $chunk_metadata
611 );
612 } else {
613 // Store in WordPress DB with chunk metadata
614 $content_with_metadata = MxChat_Chunker::format_chunk_for_storage($chunk_text, $chunk_metadata);
615 $embedding_vector_serialized = maybe_serialize($embedding_vector);
616
617 $result = self::store_chunk_in_wordpress_db(
618 $content_with_metadata,
619 $source_url,
620 $embedding_vector_serialized,
621 $table_name,
622 $content_type,
623 $chunk_metadata
624 );
625 }
626
627 if (is_wp_error($result)) {
628 $errors[] = $result;
629 //error_log('[MXCHAT-CHUNK] Failed to store chunk ' . $index . ': ' . $result->get_error_message());
630 }
631 }
632
633 if (count($errors) === $total_chunks) {
634 return new WP_Error('chunking_failed', 'Failed to store any chunks');
635 }
636
637 if (!empty($errors)) {
638 //error_log('[MXCHAT-CHUNK] Completed with ' . count($errors) . ' errors out of ' . $total_chunks . ' chunks');
639 return new WP_Error('chunking_partial_failure',
640 sprintf('Failed to store %d of %d chunks', count($errors), $total_chunks));
641 }
642
643 //error_log('[MXCHAT-CHUNK] Successfully stored all ' . $total_chunks . ' chunks');
644 return true;
645 }
646
647 /**
648 * Store a single chunk in Pinecone with chunk-specific metadata
649 */
650 private static function store_chunk_in_pinecone($embedding_vector, $chunk_text, $source_url, $vector_id, $bot_id, $content_type, $chunk_metadata) {
651 // Get Pinecone configuration
652 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
653 $pinecone_options = get_option('mxchat_pinecone_addon_options');
654 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
655 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
656 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
657 } else {
658 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
659 if (empty($bot_pinecone_config)) {
660 $pinecone_options = get_option('mxchat_pinecone_addon_options');
661 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
662 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
663 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
664 } else {
665 $api_key = $bot_pinecone_config['api_key'] ?? '';
666 $host = $bot_pinecone_config['host'] ?? '';
667 $namespace = $bot_pinecone_config['namespace'] ?? '';
668 }
669 }
670
671 if (empty($host) || empty($api_key)) {
672 return new WP_Error('pinecone_config', 'Pinecone is not properly configured');
673 }
674
675 $api_endpoint = "https://{$host}/vectors/upsert";
676
677 // Build metadata with chunk information
678 $metadata = array(
679 'text' => $chunk_text,
680 'source_url' => $source_url,
681 'type' => $content_type,
682 'is_chunked' => true,
683 'chunk_index' => $chunk_metadata['chunk_index'],
684 'total_chunks' => $chunk_metadata['total_chunks'],
685 'parent_url_hash' => $chunk_metadata['parent_url_hash'],
686 'last_updated' => time(),
687 'created_at' => time(),
688 'bot_id' => $bot_id
689 );
690
691 $vector_data = array(
692 'id' => $vector_id,
693 'values' => $embedding_vector,
694 'metadata' => $metadata
695 );
696
697 $request_body = array(
698 'vectors' => array($vector_data)
699 );
700
701 if (!empty($namespace)) {
702 $request_body['namespace'] = $namespace;
703 }
704
705 $response = wp_remote_post($api_endpoint, array(
706 'headers' => array(
707 'Api-Key' => $api_key,
708 'accept' => 'application/json',
709 'content-type' => 'application/json'
710 ),
711 'body' => wp_json_encode($request_body),
712 'timeout' => 30
713 ));
714
715 if (is_wp_error($response)) {
716 return $response;
717 }
718
719 $response_code = wp_remote_retrieve_response_code($response);
720 if ($response_code !== 200) {
721 return new WP_Error('pinecone_api', 'Pinecone API error: HTTP ' . $response_code);
722 }
723
724 return true;
725 }
726
727 /**
728 * Store a single chunk in WordPress database
729 */
730 private static function store_chunk_in_wordpress_db($content_with_metadata, $source_url, $embedding_vector_serialized, $table_name, $content_type, $chunk_metadata) {
731 global $wpdb;
732
733 // For chunks, we always insert new rows (no duplicate checking)
734 // The URL includes chunk info in the metadata, but source_url stays the same for grouping
735 $result = $wpdb->insert(
736 $table_name,
737 array(
738 'url' => $source_url,
739 'article_content' => $content_with_metadata,
740 'embedding_vector' => $embedding_vector_serialized,
741 'source_url' => $source_url,
742 'content_type' => $content_type,
743 'timestamp' => current_time('mysql')
744 ),
745 array('%s', '%s', '%s', '%s', '%s', '%s')
746 );
747
748 if ($result === false) {
749 return new WP_Error('database_failed', 'Failed to insert chunk: ' . $wpdb->last_error);
750 }
751
752 return true;
753 }
754
755 /**
756 * Delete all chunks for a given URL
757 *
758 * @param string $source_url The source URL
759 * @param string $bot_id The bot ID
760 * @return bool|WP_Error True on success, WP_Error on failure
761 */
762 public static function delete_chunks_for_url($source_url, $bot_id = 'default') {
763 //error_log('[MXCHAT-CHUNK-DELETE] Deleting chunks for URL: ' . $source_url);
764
765 if (self::is_pinecone_enabled_for_bot($bot_id)) {
766 return self::delete_pinecone_chunks_by_url($source_url, $bot_id);
767 } else {
768 return self::delete_wordpress_chunks_by_url($source_url);
769 }
770 }
771
772 /**
773 * Delete all chunks for a URL from Pinecone
774 */
775 private static function delete_pinecone_chunks_by_url($source_url, $bot_id) {
776 // Get Pinecone configuration
777 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
778 $pinecone_options = get_option('mxchat_pinecone_addon_options');
779 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
780 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
781 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
782 } else {
783 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
784 if (empty($bot_pinecone_config)) {
785 $pinecone_options = get_option('mxchat_pinecone_addon_options');
786 $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
787 $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
788 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
789 } else {
790 $api_key = $bot_pinecone_config['api_key'] ?? '';
791 $host = $bot_pinecone_config['host'] ?? '';
792 $namespace = $bot_pinecone_config['namespace'] ?? '';
793 }
794 }
795
796 if (empty($host) || empty($api_key)) {
797 return new WP_Error('pinecone_config', 'Pinecone is not properly configured');
798 }
799
800 $base_vector_id = md5($source_url);
801 $vectors_to_delete = array();
802
803 // Add the original single-vector ID (for non-chunked content)
804 $vectors_to_delete[] = $base_vector_id;
805
806 // Use Pinecone list API to find all chunk vectors with this prefix
807 $list_url = "https://{$host}/vectors/list";
808
809 $list_body = array(
810 'prefix' => $base_vector_id . '_chunk_',
811 'limit' => 100
812 );
813
814 if (!empty($namespace)) {
815 $list_body['namespace'] = $namespace;
816 }
817
818 $list_response = wp_remote_post($list_url, array(
819 'headers' => array(
820 'Api-Key' => $api_key,
821 'accept' => 'application/json',
822 'content-type' => 'application/json'
823 ),
824 'body' => wp_json_encode($list_body),
825 'timeout' => 30
826 ));
827
828 if (!is_wp_error($list_response)) {
829 $list_data = json_decode(wp_remote_retrieve_body($list_response), true);
830 if (!empty($list_data['vectors'])) {
831 foreach ($list_data['vectors'] as $vector) {
832 if (isset($vector['id'])) {
833 $vectors_to_delete[] = $vector['id'];
834 }
835 }
836 }
837 }
838
839 if (empty($vectors_to_delete)) {
840 //error_log('[MXCHAT-CHUNK-DELETE] No vectors found to delete');
841 return true;
842 }
843
844 //error_log('[MXCHAT-CHUNK-DELETE] Deleting ' . count($vectors_to_delete) . ' vectors from Pinecone');
845
846 // Delete vectors
847 $delete_url = "https://{$host}/vectors/delete";
848
849 $delete_body = array(
850 'ids' => $vectors_to_delete
851 );
852
853 if (!empty($namespace)) {
854 $delete_body['namespace'] = $namespace;
855 }
856
857 $delete_response = wp_remote_post($delete_url, array(
858 'headers' => array(
859 'Api-Key' => $api_key,
860 'accept' => 'application/json',
861 'content-type' => 'application/json'
862 ),
863 'body' => wp_json_encode($delete_body),
864 'timeout' => 30
865 ));
866
867 if (is_wp_error($delete_response)) {
868 return $delete_response;
869 }
870
871 $response_code = wp_remote_retrieve_response_code($delete_response);
872 if ($response_code !== 200) {
873 return new WP_Error('pinecone_delete', 'Failed to delete vectors: HTTP ' . $response_code);
874 }
875
876 return true;
877 }
878
879 /**
880 * Delete all chunks for a URL from WordPress database
881 */
882 private static function delete_wordpress_chunks_by_url($source_url) {
883 global $wpdb;
884 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
885
886 // Delete all rows with this source_url (handles both chunked and non-chunked)
887 $result = $wpdb->delete(
888 $table_name,
889 array('source_url' => $source_url),
890 array('%s')
891 );
892
893 if ($result === false) {
894 return new WP_Error('database_delete', 'Failed to delete chunks: ' . $wpdb->last_error);
895 }
896
897 //error_log('[MXCHAT-CHUNK-DELETE] Deleted ' . $result . ' rows from WordPress DB');
898 return true;
899 }
900 }