PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 2.3.5
MxChat – AI Chatbot & Content Generation for WordPress v2.3.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
← All changes | includes/class-mxchat-utils.php +359 -72 2.0.42.3.5 View file →
@@ -4,112 +4,399 @@
4 4 }
5 5
6 6 class MxChat_Utils {
7 7
8 - /**
9 - * Submit or update content (and its embedding) in the database.
10 - *
11 - * If the source_url already exists, update that row.
12 - * Otherwise, insert a new row.
13 - *
14 - * @param string $content The content to be embedded.
15 - * @param string $source_url The source URL of the content.
16 - * @param string $api_key The API key used for generating embeddings.
17 - */
18 - public static function submit_content_to_db($content, $source_url, $api_key) {
19 - global $wpdb;
20 - $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
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);
21 32
22 - // Sanitize the source URL
23 - $source_url = esc_url_raw($source_url);
24 33
25 - // Generate the embedding using the API key
26 - $embedding_vector = self::generate_embedding($content, $api_key);
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 +}
27 56
28 - if (is_array($embedding_vector)) {
29 - $embedding_vector_serialized = maybe_serialize($embedding_vector);
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 +}
30 73
31 - // Check if a row already exists for this URL
32 - $existing_id = $wpdb->get_var(
33 - $wpdb->prepare(
34 - "SELECT id FROM {$table_name} WHERE source_url = %s LIMIT 1",
35 - $source_url
36 - )
37 - );
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 +}
38 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 {
39 134 if ($existing_id) {
135 + //error_log('[MXCHAT-DB] Found existing entry (ID: ' . $existing_id . '). Updating... (Attempt ' . $attempt . ')');
136 +
40 137 // Update the existing row
41 - $wpdb->update(
138 + $result = $wpdb->update(
42 139 $table_name,
43 140 array(
44 - 'article_content' => $content,
141 + 'url' => $source_url,
142 + 'article_content' => $current_content,
45 143 'embedding_vector' => $embedding_vector_serialized,
46 - 'timestamp' => current_time('mysql'), // Remove if your table doesn't have a timestamp column
144 + 'source_url' => $source_url,
145 + 'timestamp' => current_time('mysql'),
47 146 ),
48 147 array('id' => $existing_id),
49 - array('%s','%s','%s'),
148 + array('%s','%s','%s','%s','%s'),
50 149 array('%d')
51 150 );
52 151 } else {
53 - // Insert a new row
54 - $wpdb->insert(
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(
55 157 $table_name,
56 158 array(
57 - 'article_content' => $content,
159 + 'url' => $source_url, // Can be empty for manual content
160 + 'article_content' => $current_content,
58 161 'embedding_vector' => $embedding_vector_serialized,
59 - 'source_url' => $source_url,
60 - 'timestamp' => current_time('mysql'), // Remove if your table doesn't have a timestamp column
162 + 'source_url' => $source_url, // Can be empty for manual content
163 + 'timestamp' => current_time('mysql'),
61 164 ),
62 - array('%s','%s','%s','%s')
165 + array('%s','%s','%s','%s','%s')
63 166 );
64 167 }
65 - } else {
66 - // If embedding generation failed, you could log or handle it here
67 - // error_log('Embedding generation failed for content from ' . $source_url);
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++;
68 188 }
69 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 +}
70 199
71 - /**
72 - * Generate an embedding for the given text using the specified API key.
73 - *
74 - * @param string $text The text to be embedded.
75 - * @param string $api_key The API key used for generating embeddings.
76 - * @return array|null The embedding vector or null on failure.
77 - */
78 - private static function generate_embedding($text, $api_key) {
79 - $endpoint = 'https://api.openai.com/v1/embeddings';
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);
80 228
81 - $body = wp_json_encode([
82 - 'input' => $text,
83 - 'model' => 'text-embedding-ada-002'
84 - ]);
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 + }
85 233
86 - $args = [
87 - 'body' => $body,
88 - 'headers' => [
89 - 'Content-Type' => 'application/json',
90 - 'Authorization' => 'Bearer ' . $api_key,
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 + ]
91 336 ],
92 - 'timeout' => 60,
93 - 'redirection' => 5,
94 - 'blocking' => true,
95 - 'httpversion' => '1.0',
96 - 'sslverify' => true,
337 + 'outputDimensionality' => 1536
97 338 ];
98 -
99 - $response = wp_remote_post($endpoint, $args);
100 -
101 - if (is_wp_error($response)) {
102 - // error_log('Error generating embedding: ' . $response->get_error_message());
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));
103 390 return null;
104 391 }
105 -
106 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
107 -
392 + } else {
393 + // OpenAI/Voyage API response format
108 394 if (isset($response_body['data'][0]['embedding']) && is_array($response_body['data'][0]['embedding'])) {
109 395 return $response_body['data'][0]['embedding'];
110 396 } else {
111 - // error_log('Invalid response received from embedding API.');
397 + //error_log('Invalid response received from embedding API: ' . wp_json_encode($response_body));
112 398 return null;
113 399 }
114 400 }
115 -}
401 +}
402 +}