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

373 lines 14.8 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 // Check if a row already exists for this URL
110 $existing_id = $wpdb->get_var(
111 $wpdb->prepare(
112 "SELECT id FROM {$table_name} WHERE source_url = %s LIMIT 1",
113 $source_url
114 )
115 );
116
117 // Progressive fallback mechanism for problematic content
118 $attempt = 1;
119 $max_attempts = 3;
120 $current_content = $safe_content;
121 $result = false;
122
123 while ($attempt <= $max_attempts && $result === false) {
124 try {
125 if ($existing_id) {
126 error_log('[MXCHAT-DB] Found existing entry (ID: ' . $existing_id . '). Updating... (Attempt ' . $attempt . ')');
127
128 // Update the existing row
129 $result = $wpdb->update(
130 $table_name,
131 array(
132 'url' => $source_url,
133 'article_content' => $current_content,
134 'embedding_vector' => $embedding_vector_serialized,
135 'source_url' => $source_url,
136 'timestamp' => current_time('mysql'),
137 ),
138 array('id' => $existing_id),
139 array('%s','%s','%s','%s','%s'),
140 array('%d')
141 );
142 } else {
143 error_log('[MXCHAT-DB] No existing entry found. Inserting new row... (Attempt ' . $attempt . ')');
144 error_log('[MXCHAT-DB] Content sample: ' . substr($current_content, 0, 1000));
145
146 // Insert a new row
147 $result = $wpdb->insert(
148 $table_name,
149 array(
150 'url' => $source_url,
151 'article_content' => $current_content,
152 'embedding_vector' => $embedding_vector_serialized,
153 'source_url' => $source_url,
154 'timestamp' => current_time('mysql'),
155 ),
156 array('%s','%s','%s','%s','%s')
157 );
158 }
159
160 if ($result === false) {
161 error_log('[MXCHAT-DB] Database operation failed (Attempt ' . $attempt . '): ' . $wpdb->last_error);
162
163 // Progressively apply more aggressive sanitization on failure
164 if ($attempt === 1) {
165 // First fallback: Use a more aggressive character filter and shorten
166 $current_content = preg_replace('/[^\p{L}\p{N}\s.,;:!?()-]/u', '', $current_content);
167 $current_content = substr($current_content, 0, 50000);
168 } else if ($attempt === 2) {
169 // Second fallback: Keep only alphanumeric and basic punctuation, shorten further
170 $current_content = preg_replace('/[^a-zA-Z0-9\s.,;:!?()-]/u', '', $current_content);
171 $current_content = substr($current_content, 0, 30000);
172 }
173
174 $attempt++;
175 }
176 } catch (Exception $e) {
177 error_log('[MXCHAT-DB] Exception during database operation: ' . $e->getMessage());
178 $attempt++;
179 }
180 }
181
182 if ($result === false) {
183 error_log('[MXCHAT-DB] All database operation attempts failed');
184 return new WP_Error('database_failed', 'Failed to store content in WordPress database after ' . $max_attempts . ' attempts');
185 }
186
187 error_log('[MXCHAT-DB] WordPress database operation completed successfully (Attempt ' . ($attempt - 1) . ')');
188 return true;
189 }
190
191 /**
192 * Store content in Pinecone database
193 */
194 private static function store_in_pinecone_main($embedding_vector, $content, $url, $api_key, $environment, $index_name, $vector_id = null) {
195 error_log('[MXCHAT-PINECONE-MAIN] ===== Starting Pinecone storage =====');
196
197 $vector_id = $vector_id ?: md5($url);
198 error_log('[MXCHAT-PINECONE-MAIN] Vector ID: ' . $vector_id);
199
200 $options = get_option('mxchat_pinecone_addon_options');
201 $host = $options['mxchat_pinecone_host'] ?? '';
202
203 error_log('[MXCHAT-PINECONE-MAIN] Host from options: ' . $host);
204 error_log('[MXCHAT-PINECONE-MAIN] API key length: ' . strlen($api_key));
205 error_log('[MXCHAT-PINECONE-MAIN] Environment: ' . $environment);
206 error_log('[MXCHAT-PINECONE-MAIN] Index name: ' . $index_name);
207
208 if (empty($host)) {
209 error_log('[MXCHAT-PINECONE-MAIN] ERROR: Host is empty');
210 return new WP_Error('pinecone_config', 'Pinecone host is not configured. Please set the host in your settings.');
211 }
212
213 // Determine if this is a product URL
214 $is_product = (strpos($url, '/product/') !== false || strpos($url, '/shop/') !== false);
215 error_log('[MXCHAT-PINECONE-MAIN] Is product URL: ' . ($is_product ? 'YES' : 'NO'));
216
217 $api_endpoint = "https://{$host}/vectors/upsert";
218 error_log('[MXCHAT-PINECONE-MAIN] API endpoint: ' . $api_endpoint);
219
220 $request_body = array(
221 'vectors' => array(
222 array(
223 'id' => $vector_id,
224 'values' => $embedding_vector,
225 'metadata' => array(
226 'text' => $content,
227 'source_url' => $url,
228 'type' => $is_product ? 'product' : 'content',
229 'last_updated' => time()
230 )
231 )
232 )
233 );
234
235 error_log('[MXCHAT-PINECONE-MAIN] Request body prepared (embedding dimensions: ' . count($embedding_vector) . ')');
236
237 $response = wp_remote_post($api_endpoint, array(
238 'headers' => array(
239 'Api-Key' => $api_key,
240 'accept' => 'application/json',
241 'content-type' => 'application/json'
242 ),
243 'body' => wp_json_encode($request_body),
244 'timeout' => 30,
245 'data_format' => 'body'
246 ));
247
248 if (is_wp_error($response)) {
249 error_log('[MXCHAT-PINECONE-MAIN] WordPress request error: ' . $response->get_error_message());
250 return new WP_Error('pinecone_request', $response->get_error_message());
251 }
252
253 $response_code = wp_remote_retrieve_response_code($response);
254 error_log('[MXCHAT-PINECONE-MAIN] Response code: ' . $response_code);
255
256 if ($response_code !== 200) {
257 $body = wp_remote_retrieve_body($response);
258 error_log('[MXCHAT-PINECONE-MAIN] API error - Response body: ' . $body);
259 return new WP_Error('pinecone_api', sprintf(
260 'Pinecone API error (HTTP %d): %s',
261 $response_code,
262 $body
263 ));
264 }
265
266 $response_body = wp_remote_retrieve_body($response);
267 error_log('[MXCHAT-PINECONE-MAIN] Success response: ' . $response_body);
268 error_log('[MXCHAT-PINECONE-MAIN] Successfully stored in Pinecone');
269 error_log('[MXCHAT-PINECONE-MAIN] ===== Pinecone storage complete =====');
270
271 return true;
272 }
273
274 /**
275 * Generate an embedding for the given text using the specified API key.
276 *
277 * @param string $text The text to be embedded.
278 * @param string $api_key The API key used for generating embeddings.
279 * @return array|null The embedding vector or null on failure.
280 */
281 private static function generate_embedding($text, $api_key) {
282 // Get options and selected model
283 $options = get_option('mxchat_options');
284 $selected_model = $options['embedding_model'] ?? 'text-embedding-ada-002';
285
286 // Determine endpoint and API key based on model
287 if (strpos($selected_model, 'voyage') === 0) {
288 $endpoint = 'https://api.voyageai.com/v1/embeddings';
289 $api_key = $options['voyage_api_key'] ?? '';
290 } elseif (strpos($selected_model, 'gemini-embedding') === 0) {
291 $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . $selected_model . ':embedContent';
292 $api_key = $options['gemini_api_key'] ?? '';
293 } else {
294 $endpoint = 'https://api.openai.com/v1/embeddings';
295 // Use the passed API key for OpenAI
296 }
297
298 // Prepare request body based on provider
299 if (strpos($selected_model, 'gemini-embedding') === 0) {
300 // Gemini API format
301 $request_body = [
302 'model' => 'models/' . $selected_model,
303 'content' => [
304 'parts' => [
305 ['text' => $text]
306 ]
307 ],
308 'outputDimensionality' => 1536
309 ];
310
311 // Prepare headers for Gemini (API key as query parameter)
312 $endpoint .= '?key=' . $api_key;
313 $headers = [
314 'Content-Type' => 'application/json'
315 ];
316 } else {
317 // OpenAI/Voyage API format
318 $request_body = [
319 'input' => $text,
320 'model' => $selected_model
321 ];
322
323 // Add output_dimension for voyage-3-large
324 if ($selected_model === 'voyage-3-large') {
325 $request_body['output_dimension'] = 2048;
326 }
327
328 // Prepare headers for OpenAI/Voyage
329 $headers = [
330 'Content-Type' => 'application/json',
331 'Authorization' => 'Bearer ' . $api_key
332 ];
333 }
334
335 $args = [
336 'body' => wp_json_encode($request_body),
337 'headers' => $headers,
338 'timeout' => 60,
339 'redirection' => 5,
340 'blocking' => true,
341 'httpversion' => '1.0',
342 'sslverify' => true,
343 ];
344
345 $response = wp_remote_post($endpoint, $args);
346
347 if (is_wp_error($response)) {
348 error_log('Error generating embedding: ' . $response->get_error_message());
349 return null;
350 }
351
352 $response_body = json_decode(wp_remote_retrieve_body($response), true);
353
354 // Handle different response formats based on provider
355 if (strpos($selected_model, 'gemini-embedding') === 0) {
356 // Gemini API response format
357 if (isset($response_body['embedding']['values']) && is_array($response_body['embedding']['values'])) {
358 return $response_body['embedding']['values'];
359 } else {
360 error_log('Invalid response received from Gemini embedding API: ' . wp_json_encode($response_body));
361 return null;
362 }
363 } else {
364 // OpenAI/Voyage API response format
365 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
366 return $response_body['data'][0]['embedding'];
367 } else {
368 error_log('Invalid response received from embedding API: ' . wp_json_encode($response_body));
369 return null;
370 }
371 }
372 }
373 }