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

488 lines 20.9 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 * @return bool|WP_Error True on success, WP_Error on failure
18 */
19 public static function submit_content_to_db($content, $source_url, $api_key, $vector_id = null, $bot_id = 'default') {
20 global $wpdb;
21 $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
22
23 //error_log('[MXCHAT-DB] Starting database submission for URL: ' . $source_url . ' (Bot: ' . $bot_id . ')');
24 //error_log('[MXCHAT-DB] Content length: ' . strlen($content) . ' bytes');
25
26 // Sanitize the source URL
27 $source_url = esc_url_raw($source_url);
28
29 // Just ensure UTF-8 validity without aggressive escaping
30 $safe_content = wp_check_invalid_utf8($content);
31 // Remove only null bytes and other control characters, but preserve newlines (\n = \x0A) and carriage returns (\r = \x0D)
32 $safe_content = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $safe_content);
33
34 // UPDATED: Generate the embedding using bot-specific configuration
35 $embedding_vector = self::generate_embedding($content, $api_key, $bot_id);
36
37 if (!is_array($embedding_vector)) {
38 //error_log('[MXCHAT-DB] Error: Embedding generation failed');
39 return new WP_Error('embedding_failed', 'Failed to generate embedding for content');
40 }
41
42 //error_log('[MXCHAT-DB] Embedding generated successfully');
43
44 // UPDATED: Check if Pinecone is enabled for this specific bot
45 if (self::is_pinecone_enabled_for_bot($bot_id)) {
46 //error_log('[MXCHAT-DB] Pinecone is enabled for bot ' . $bot_id . ' - using Pinecone storage');
47 // Store in Pinecone only
48 return self::store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id, $bot_id);
49 } else {
50 //error_log('[MXCHAT-DB] Pinecone not enabled for bot ' . $bot_id . ' - using WordPress storage');
51 // Store in WordPress database only
52 $embedding_vector_serialized = maybe_serialize($embedding_vector);
53 return self::store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name);
54 }
55 }
56
57 /**
58 * UPDATED: Check if Pinecone is enabled and properly configured for a specific bot
59 */
60 private static function is_pinecone_enabled_for_bot($bot_id = 'default') {
61 // For default bot or when multi-bot is not active, use original method
62 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
63 return self::is_pinecone_enabled();
64 }
65
66 // Get bot-specific Pinecone configuration
67 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
68
69 if (empty($bot_pinecone_config)) {
70 // Fallback to default configuration
71 return self::is_pinecone_enabled();
72 }
73
74 $enabled_check = !empty($bot_pinecone_config['use_pinecone']) && $bot_pinecone_config['use_pinecone'];
75 $api_key_check = !empty($bot_pinecone_config['api_key']);
76 $host_check = !empty($bot_pinecone_config['host']);
77
78 return $enabled_check && $api_key_check && $host_check;
79 }
80
81 /**
82 * Check if Pinecone is enabled and properly configured (original method for default bot)
83 */
84 private static function is_pinecone_enabled() {
85 $pinecone_options = get_option('mxchat_pinecone_addon_options');
86
87 if (empty($pinecone_options)) {
88 return false;
89 }
90
91 $enabled_check = !empty($pinecone_options['mxchat_use_pinecone']) && $pinecone_options['mxchat_use_pinecone'] !== '0';
92 $api_key_check = !empty($pinecone_options['mxchat_pinecone_api_key']);
93 $host_check = !empty($pinecone_options['mxchat_pinecone_host']);
94
95 return $enabled_check && $api_key_check && $host_check;
96 }
97
98 /**
99 * UPDATED: Store content in Pinecone only with bot support
100 */
101 private static function store_in_pinecone_only($embedding_vector, $content, $source_url, $vector_id = null, $bot_id = 'default') {
102 //error_log('[MXCHAT-PINECONE] ===== Using Pinecone-only storage for bot ' . $bot_id . ' =====');
103
104 // Get bot-specific Pinecone configuration
105 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
106 $pinecone_options = get_option('mxchat_pinecone_addon_options');
107 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
108 $environment = $pinecone_options['mxchat_pinecone_environment'] ?? '';
109 $index_name = $pinecone_options['mxchat_pinecone_index'] ?? '';
110 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
111 } else {
112 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
113 if (empty($bot_pinecone_config)) {
114 // Fallback to default configuration
115 $pinecone_options = get_option('mxchat_pinecone_addon_options');
116 $api_key = $pinecone_options['mxchat_pinecone_api_key'];
117 $environment = $pinecone_options['mxchat_pinecone_environment'] ?? '';
118 $index_name = $pinecone_options['mxchat_pinecone_index'] ?? '';
119 $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
120 } else {
121 $api_key = $bot_pinecone_config['api_key'];
122 $environment = ''; // Not used in new Pinecone API
123 $index_name = ''; // Not used in new Pinecone API
124 $namespace = $bot_pinecone_config['namespace'] ?? '';
125 }
126 }
127
128 $result = self::store_in_pinecone_main(
129 $embedding_vector,
130 $content,
131 $source_url,
132 $api_key,
133 $environment,
134 $index_name,
135 $vector_id,
136 $bot_id,
137 $namespace
138 );
139
140 if (is_wp_error($result)) {
141 //error_log('[MXCHAT-PINECONE] Pinecone storage failed for bot ' . $bot_id . ': ' . $result->get_error_message());
142 return $result;
143 }
144
145 //error_log('[MXCHAT-PINECONE] Pinecone storage completed successfully for bot ' . $bot_id);
146 return true;
147 }
148
149 /**
150 * Store content in WordPress database with progressive fallback (unchanged)
151 */
152 private static function store_in_wordpress_db($safe_content, $source_url, $embedding_vector_serialized, $table_name) {
153 global $wpdb;
154
155 //error_log('[MXCHAT-DB] ===== Using WordPress-only storage =====');
156
157 // ===== FIXED: Generate unique identifier for manual content =====
158 $original_source_url = $source_url;
159 $is_manual_content = empty($source_url) || $source_url === '' || !filter_var($source_url, FILTER_VALIDATE_URL);
160
161 if ($is_manual_content) {
162 // Generate unique identifier for manual content to prevent overwrites
163 $source_url = 'mxchat://manual-content/' . time() . '-' . wp_generate_password(8, false);
164 //error_log('[MXCHAT-DB] Generated unique ID for manual content: ' . $source_url);
165 }
166
167 // Only check for duplicates if we have a valid source URL (not manual content)
168 $existing_id = null;
169 if (!$is_manual_content) {
170 $existing_id = $wpdb->get_var(
171 $wpdb->prepare(
172 "SELECT id FROM {$table_name} WHERE source_url = %s LIMIT 1",
173 $source_url
174 )
175 );
176 //error_log('[MXCHAT-DB] Checked for existing URL, found ID: ' . ($existing_id ?: 'none'));
177 } else {
178 //error_log('[MXCHAT-DB] Manual content - will create new entry (no duplicate check)');
179 }
180 // ===== END FIX =====
181
182 // Progressive fallback mechanism for problematic content
183 $attempt = 1;
184 $max_attempts = 3;
185 $current_content = $safe_content;
186 $result = false;
187
188 while ($attempt <= $max_attempts && $result === false) {
189 try {
190 if ($existing_id) {
191 //error_log('[MXCHAT-DB] Found existing entry (ID: ' . $existing_id . '). Updating... (Attempt ' . $attempt . ')');
192
193 // Update the existing row
194 $result = $wpdb->update(
195 $table_name,
196 array(
197 'url' => $source_url,
198 'article_content' => $current_content,
199 'embedding_vector' => $embedding_vector_serialized,
200 'source_url' => $source_url,
201 'timestamp' => current_time('mysql'),
202 ),
203 array('id' => $existing_id),
204 array('%s','%s','%s','%s','%s'),
205 array('%d')
206 );
207 } else {
208 //error_log('[MXCHAT-DB] No existing entry found. Inserting new row... (Attempt ' . $attempt . ')');
209 //error_log('[MXCHAT-DB] Content sample: ' . substr($current_content, 0, 1000));
210
211 // Insert a new row (using generated unique ID for manual content)
212 $result = $wpdb->insert(
213 $table_name,
214 array(
215 'url' => $source_url, // Now unique for manual content
216 'article_content' => $current_content,
217 'embedding_vector' => $embedding_vector_serialized,
218 'source_url' => $source_url, // Now unique for manual content
219 'timestamp' => current_time('mysql'),
220 ),
221 array('%s','%s','%s','%s','%s')
222 );
223 }
224
225 if ($result === false) {
226 //error_log('[MXCHAT-DB] Database operation failed (Attempt ' . $attempt . '): ' . $wpdb->last_error);
227
228 // Progressively apply more aggressive sanitization on failure
229 if ($attempt === 1) {
230 // First fallback: Use a more aggressive character filter and shorten
231 $current_content = preg_replace('/[^\p{L}\p{N}\s.,;:!?()-]/u', '', $current_content);
232 $current_content = substr($current_content, 0, 50000);
233 } else if ($attempt === 2) {
234 // Second fallback: Keep only alphanumeric and basic punctuation, shorten further
235 $current_content = preg_replace('/[^a-zA-Z0-9\s.,;:!?()-]/u', '', $current_content);
236 $current_content = substr($current_content, 0, 30000);
237 }
238
239 $attempt++;
240 }
241 } catch (Exception $e) {
242 //error_log('[MXCHAT-DB] Exception during database operation: ' . $e->getMessage());
243 $attempt++;
244 }
245 }
246
247 if ($result === false) {
248 //error_log('[MXCHAT-DB] All database operation attempts failed');
249 return new WP_Error('database_failed', 'Failed to store content in WordPress database after ' . $max_attempts . ' attempts');
250 }
251
252 //error_log('[MXCHAT-DB] WordPress database operation completed successfully (Attempt ' . ($attempt - 1) . ')');
253 return true;
254 }
255
256 /**
257 * UPDATED: Store content in Pinecone database with bot support
258 */
259 private static function store_in_pinecone_main($embedding_vector, $content, $url, $api_key, $environment, $index_name, $vector_id = null, $bot_id = 'default', $namespace = '') {
260 //error_log('[MXCHAT-PINECONE-MAIN] ===== Starting Pinecone storage for bot ' . $bot_id . ' =====');
261
262 // ===== UPDATED: Handle manual content with unique vector IDs =====
263 if ($vector_id) {
264 // Use provided vector ID
265 //error_log('[MXCHAT-PINECONE-MAIN] Using provided vector ID: ' . $vector_id);
266 } elseif (!empty($url) && filter_var($url, FILTER_VALIDATE_URL)) {
267 // For valid URLs, use URL-based ID (existing behavior)
268 $vector_id = md5($url);
269 //error_log('[MXCHAT-PINECONE-MAIN] Generated vector ID from URL: ' . $vector_id);
270 } else {
271 // For manual content (empty/invalid URL), generate unique ID
272 $vector_id = 'manual_' . time() . '_' . substr(md5($content . microtime(true)), 0, 8);
273 //error_log('[MXCHAT-PINECONE-MAIN] Generated unique vector ID for manual content: ' . $vector_id);
274 }
275 // ===== END UPDATE =====
276
277 // Get host from bot-specific config or fallback to default
278 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
279 $options = get_option('mxchat_pinecone_addon_options');
280 $host = $options['mxchat_pinecone_host'] ?? '';
281 } else {
282 $bot_pinecone_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
283 if (!empty($bot_pinecone_config)) {
284 $host = $bot_pinecone_config['host'] ?? '';
285 } else {
286 $options = get_option('mxchat_pinecone_addon_options');
287 $host = $options['mxchat_pinecone_host'] ?? '';
288 }
289 }
290
291 //error_log('[MXCHAT-PINECONE-MAIN] Host: ' . $host);
292 //error_log('[MXCHAT-PINECONE-MAIN] API key length: ' . strlen($api_key));
293 //error_log('[MXCHAT-PINECONE-MAIN] Bot ID: ' . $bot_id);
294 //error_log('[MXCHAT-PINECONE-MAIN] Namespace: ' . $namespace);
295
296 if (empty($host)) {
297 //error_log('[MXCHAT-PINECONE-MAIN] ERROR: Host is empty');
298 return new WP_Error('pinecone_config', 'Pinecone host is not configured. Please set the host in your bot settings.');
299 }
300
301 // ===== UPDATED: Determine content type more accurately =====
302 $is_product = false;
303 $content_type = 'manual'; // Default for manual content
304
305 if (!empty($url) && filter_var($url, FILTER_VALIDATE_URL)) {
306 $is_product = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
307 $content_type = $is_product ? 'product' : 'content';
308 }
309
310 //error_log('[MXCHAT-PINECONE-MAIN] Content type: ' . $content_type);
311 // ===== END UPDATE =====
312
313 $api_endpoint = "https://{$host}/vectors/upsert";
314 //error_log('[MXCHAT-PINECONE-MAIN] API endpoint: ' . $api_endpoint);
315
316 // UPDATED: Add bot_id to metadata and handle namespace
317 $metadata = array(
318 'text' => $content,
319 'source_url' => $url, // Can be empty for manual content
320 'type' => $content_type, // 'manual', 'content', or 'product'
321 'last_updated' => time(),
322 'created_at' => time(), // Add creation timestamp
323 'bot_id' => $bot_id // Add bot identification
324 );
325
326 $vector_data = array(
327 'id' => $vector_id,
328 'values' => $embedding_vector,
329 'metadata' => $metadata
330 );
331
332 $request_body = array(
333 'vectors' => array($vector_data)
334 );
335
336 // Add namespace if specified for multi-bot separation
337 if (!empty($namespace)) {
338 $request_body['namespace'] = $namespace;
339 //error_log('[MXCHAT-PINECONE-MAIN] Using namespace: ' . $namespace);
340 }
341
342 //error_log('[MXCHAT-PINECONE-MAIN] Request body prepared (embedding dimensions: ' . count($embedding_vector) . ')');
343
344 $response = wp_remote_post($api_endpoint, array(
345 'headers' => array(
346 'Api-Key' => $api_key,
347 'accept' => 'application/json',
348 'content-type' => 'application/json'
349 ),
350 'body' => wp_json_encode($request_body),
351 'timeout' => 30,
352 'data_format' => 'body'
353 ));
354
355 if (is_wp_error($response)) {
356 //error_log('[MXCHAT-PINECONE-MAIN] WordPress request error: ' . $response->get_error_message());
357 return new WP_Error('pinecone_request', $response->get_error_message());
358 }
359
360 $response_code = wp_remote_retrieve_response_code($response);
361 //error_log('[MXCHAT-PINECONE-MAIN] Response code: ' . $response_code);
362
363 if ($response_code !== 200) {
364 $body = wp_remote_retrieve_body($response);
365 //error_log('[MXCHAT-PINECONE-MAIN] API error - Response body: ' . $body);
366 return new WP_Error('pinecone_api', sprintf(
367 'Pinecone API error (HTTP %d): %s',
368 $response_code,
369 $body
370 ));
371 }
372
373 $response_body = wp_remote_retrieve_body($response);
374 //error_log('[MXCHAT-PINECONE-MAIN] Success response: ' . $response_body);
375 //error_log('[MXCHAT-PINECONE-MAIN] Successfully stored in Pinecone for bot ' . $bot_id);
376 //error_log('[MXCHAT-PINECONE-MAIN] ===== Pinecone storage complete =====');
377
378 return true;
379 }
380
381 /**
382 * UPDATED: Generate an embedding for the given text using bot-specific configuration.
383 *
384 * @param string $text The text to be embedded.
385 * @param string $api_key The API key used for generating embeddings.
386 * @param string $bot_id The bot ID for multi-bot support
387 * @return array|null The embedding vector or null on failure.
388 */
389 private static function generate_embedding($text, $api_key, $bot_id = 'default') {
390 // Get bot-specific options
391 if ($bot_id === 'default' || !class_exists('MxChat_Multi_Bot_Manager')) {
392 $options = get_option('mxchat_options');
393 } else {
394 $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
395 $options = !empty($bot_options) ? $bot_options : get_option('mxchat_options');
396 }
397
398 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
399
400 // Determine endpoint and API key based on model
401 if (strpos($selected_model, 'voyage') === 0) {
402 $endpoint = 'https://api.voyageai.com/v1/embeddings';
403 $api_key = $options['voyage_api_key'] ?? '';
404 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
405 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
406 $api_key = $options['gemini_api_key'] ?? '';
407 } else {
408 $endpoint = 'https://api.openai.com/v1/embeddings';
409 // Use the bot-specific API key or fallback to passed API key
410 $api_key = $options['api_key'] ?? $api_key;
411 }
412
413 // Prepare request body based on provider
414 if (strpos($selected_model, 'gemini-embedding') === 0) {
415 // Gemini API format
416 $request_body = [
417 'model' => 'models/' . $selected_model,
418 'content' => [
419 'parts' => [
420 ['text' => $text]
421 ]
422 ],
423 'outputDimensionality' => 1536
424 ];
425
426 // Prepare headers for Gemini (API key as query parameter)
427 $endpoint .= '?key=' . $api_key;
428 $headers = [
429 'Content-Type' => 'application/json'
430 ];
431 } else {
432 // OpenAI/Voyage API format
433 $request_body = [
434 'input' => $text,
435 'model' => $selected_model
436 ];
437
438 // Add output_dimension for voyage-3-large
439 if ($selected_model === 'voyage-3-large') {
440 $request_body['output_dimension'] = 2048;
441 }
442
443 // Prepare headers for OpenAI/Voyage
444 $headers = [
445 'Content-Type' => 'application/json',
446 'Authorization' => 'Bearer ' . $api_key
447 ];
448 }
449
450 $args = [
451 'body' => wp_json_encode($request_body),
452 'headers' => $headers,
453 'timeout' => 60,
454 'redirection' => 5,
455 'blocking' => true,
456 'httpversion' => '1.0',
457 'sslverify' => true,
458 ];
459
460 $response = wp_remote_post($endpoint, $args);
461
462 if (is_wp_error($response)) {
463 //error_log('Error generating embedding for bot ' . $bot_id . ': ' . $response->get_error_message());
464 return null;
465 }
466
467 $response_body = json_decode(wp_remote_retrieve_body($response), true);
468
469 // Handle different response formats based on provider
470 if (strpos($selected_model, 'gemini-embedding') === 0) {
471 // Gemini API response format
472 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
473 return $response_body['embedding']['values'];
474 } else {
475 //error_log('Invalid response received from Gemini embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
476 return null;
477 }
478 } else {
479 // OpenAI/Voyage API response format
480 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
481 return $response_body['data'][0]['embedding'];
482 } else {
483 //error_log('Invalid response received from embedding API for bot ' . $bot_id . ': ' . wp_json_encode($response_body));
484 return null;
485 }
486 }
487 }
488 }