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

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