PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.0.9
MxChat – AI Chatbot & Content Generation for WordPress v3.0.9
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.0.9, at includes/class-mxchat-utils.php

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