PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.6
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / modules / discussions.php
ai-engine / classes / modules Last commit date
advisor.php 1 year ago chatbot.php 11 months ago discussions.php 11 months ago files.php 11 months ago gdpr.php 11 months ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 1 year ago
discussions.php
746 lines
1 <?php
2
3 class Meow_MWAI_Modules_Discussions {
4 private $wpdb = null;
5 private $core = null;
6 public $table_chats = null;
7 private $db_check = false;
8 private $namespace_admin = 'mwai/v1';
9 private $namespace_ui = 'mwai-ui/v1';
10
11 public function __construct() {
12 global $wpdb;
13 $this->wpdb = $wpdb;
14 global $mwai_core;
15 $this->core = $mwai_core;
16 $this->table_chats = $wpdb->prefix . 'mwai_chats';
17
18 if ( $this->core->get_option( 'chatbot_discussions' ) ) {
19 add_filter( 'mwai_chatbot_reply', [ $this, 'chatbot_reply' ], 10, 4 );
20 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
21
22 if ( !wp_next_scheduled( 'mwai_discussions' ) ) {
23 wp_schedule_event( time(), 'hourly', 'mwai_discussions' );
24 }
25 add_action( 'mwai_discussions', [ $this, 'cron_discussions' ] );
26 }
27 }
28
29 public function rest_api_init() {
30 // Admin
31 register_rest_route( $this->namespace_admin, '/discussions/list', [
32 'methods' => 'POST',
33 'callback' => [ $this, 'rest_discussions_list' ],
34 'permission_callback' => [ $this->core, 'can_access_settings' ],
35 ] );
36 register_rest_route( $this->namespace_admin, '/discussions/delete', [
37 'methods' => 'POST',
38 'callback' => [ $this, 'rest_discussions_delete_admin' ],
39 'permission_callback' => [ $this->core, 'can_access_settings' ],
40 ] );
41
42 // UI
43 register_rest_route( $this->namespace_ui, '/discussions/list', [
44 'methods' => 'POST',
45 'callback' => [ $this, 'rest_discussions_ui_list' ],
46 'permission_callback' => '__return_true'
47 ] );
48 register_rest_route( $this->namespace_ui, '/discussions/edit', [
49 'methods' => 'POST',
50 'callback' => [ $this, 'rest_discussions_ui_edit' ],
51 'permission_callback' => '__return_true'
52 ] );
53 register_rest_route( $this->namespace_ui, '/discussions/delete', [
54 'methods' => 'POST',
55 'callback' => [ $this, 'rest_discussions_delete' ],
56 'permission_callback' => [ $this, 'can_delete_discussion' ],
57 ] );
58 }
59
60 public function can_delete_discussion( $request ) {
61 $params = $request->get_json_params();
62 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
63 $userId = get_current_user_id();
64 if ( !$userId ) {
65 return false;
66 }
67 foreach ( $chatIds as $chatId ) {
68 $chat = $this->wpdb->get_row(
69 $this->wpdb->prepare(
70 "SELECT * FROM $this->table_chats WHERE chatId = %s",
71 $chatId
72 )
73 );
74 if ( !$chat || (int) $chat->userId !== (int) $userId ) {
75 return false;
76 }
77 }
78 return true;
79 }
80
81 /**
82 * Helper method to create REST responses with automatic token refresh
83 *
84 * @param array $data The response data
85 * @param int $status HTTP status code
86 * @return WP_REST_Response
87 */
88 protected function create_rest_response( $data, $status = 200 ) {
89 // Always check if we need to provide a new nonce
90 $current_nonce = $this->core->get_nonce( true );
91 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
92
93 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
94 // We'll refresh if the nonce is older than 10 hours to be safe
95 $should_refresh = false;
96
97 if ( $request_nonce ) {
98 // Try to determine the age of the nonce
99 // WordPress uses a tick system where each tick is 12 hours
100 // If we're in the second half of the nonce's life, refresh it
101 $time = time();
102 $nonce_tick = wp_nonce_tick();
103
104 // Verify if the nonce is still valid but getting old
105 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
106 if ( $verify === 2 ) {
107 // Nonce is valid but was generated 12-24 hours ago
108 $should_refresh = true;
109 // Log will be written when token is included in response
110 }
111 }
112
113 // If the nonce has changed or should be refreshed, include the new one
114 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
115 $data['new_token'] = $current_nonce;
116
117 // Log if server debug mode is enabled
118 if ( $this->core->get_option( 'server_debug_mode' ) ) {
119 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
120 }
121 }
122
123 return new WP_REST_Response( $data, $status );
124 }
125
126 /**
127 * Generate or update the title for a specific discussion
128 * by calling the AI (if it meets the requirements).
129 *
130 * @param stdClass $discussion A row from the DB (object form).
131 * @return void
132 */
133 private function generate_title_for_discussion( $discussion ) {
134 // Check if there's already a title
135 if ( !empty( $discussion->title ) ) {
136 return; // Nothing to do if title is already set.
137 }
138
139 // Ensure it's not older than 10 days, or whatever logic you prefer
140 $ten_days_ago = strtotime( '-10 days' );
141 if ( strtotime( $discussion->updated ) < $ten_days_ago ) {
142 return; // Skip if older than 10 days
143 }
144
145 // We expect JSON in the messages
146 $messages = json_decode( $discussion->messages, true );
147 if ( !is_array( $messages ) ) {
148 return;
149 }
150
151 // Check for at least one user and one assistant message
152 $has_user_message = false;
153 $has_assistant_message = false;
154 foreach ( $messages as $message ) {
155 if ( isset( $message['role'] ) ) {
156 if ( $message['role'] === 'user' ) {
157 $has_user_message = true;
158 }
159 if ( $message['role'] === 'assistant' ) {
160 $has_assistant_message = true;
161 }
162 }
163 if ( $has_user_message && $has_assistant_message ) {
164 break;
165 }
166 }
167
168 if ( !( $has_user_message && $has_assistant_message ) ) {
169 return; // If doesn't have both, skip
170 }
171
172 // Prepare the conversation text for the prompt
173 $conversation_text = '';
174 foreach ( $messages as $message ) {
175 if ( isset( $message['role'] ) && isset( $message['content'] ) ) {
176 $role = ucfirst( $message['role'] );
177 $content = $message['content'];
178 $conversation_text .= "$role: $content\n";
179 }
180 }
181
182 $base_prompt = "Based on the following conversation, generate a concise and specific title for the discussion, strictly less than 64 characters. Focus on the main topic, avoiding unnecessary words such as articles, pronouns, or adjectives. Do not include any punctuation at the end. Do not include anything else than the title itself, only one sentence, no line breaks, just the title.\n\nConversation:\n$conversation_text\n";
183 $prompt = apply_filters( 'mwai_discussions_title_prompt', $base_prompt, $conversation_text, $discussion );
184
185 // Run the AI query using the fast environment
186 global $mwai;
187 $params = [ 'scope' => 'discussions' ];
188
189 // Use simpleFastTextQuery which handles Fast Model configuration
190 $answer = $mwai->simpleFastTextQuery( $prompt, $params );
191
192 // Clean up the answer
193 $title = trim( $answer );
194 $title = rtrim( $title, '.!?:;,—–-–' ); // Remove trailing punctuation
195 $title = substr( $title, 0, 64 ); // Ensure less than 64 characters
196 if ( empty( $title ) ) {
197 $title = 'Untitled';
198 }
199
200 // Update the discussion with the title
201 $updated = $this->wpdb->update(
202 $this->table_chats,
203 [ 'title' => $title ],
204 [ 'id' => $discussion->id ]
205 );
206 if ( $updated === false ) {
207 error_log( "Failed to update the title for discussion ID {$discussion->id}" );
208 }
209 }
210
211 /**
212 * Admin route for listing discussions. No forced logic here.
213 */
214 public function rest_discussions_list( $request ) {
215 try {
216 $params = $request->get_json_params();
217 $offset = $params['offset'];
218 $limit = $params['limit'];
219 $filters = $params['filters'];
220 $sort = $params['sort'];
221
222 // Retrieve the chats
223 $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );
224
225 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
226 }
227 catch ( Exception $e ) {
228 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
229 }
230 }
231
232 public function rest_discussions_ui_edit( $request ) {
233 try {
234 $params = $request->get_json_params();
235 $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
236 $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;
237
238 if ( is_null( $chatId ) || is_null( $title ) ) {
239 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
240 }
241
242 $userId = get_current_user_id();
243 if ( !$userId ) {
244 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
245 }
246
247 // Update the discussion title for the current user
248 $updated = $this->wpdb->update(
249 $this->table_chats,
250 [ 'title' => $title ],
251 [ 'chatId' => $chatId, 'userId' => $userId ]
252 );
253 if ( $updated === false ) {
254 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
255 }
256
257 return $this->create_rest_response( [ 'success' => true ], 200 );
258 }
259 catch ( Exception $e ) {
260 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
261 }
262 }
263
264 public function cron_discussions() {
265 $this->check_db();
266
267 // NEW CHECK: Only run if auto-titling is enabled
268 if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
269 return;
270 }
271 // END NEW CHECK
272
273 // Set the current user to the first admin to avoid guest limits
274 $admin_users = get_users( array( 'role' => 'administrator', 'number' => 1 ) );
275 if ( ! empty( $admin_users ) ) {
276 $admin_user = $admin_users[0];
277 wp_set_current_user( $admin_user->ID );
278 }
279
280 $now = date( 'Y-m-d H:i:s' );
281 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
282
283 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
284 $query = $this->wpdb->prepare(
285 "SELECT * FROM {$this->table_chats}
286 WHERE title IS NULL AND updated >= %s
287 ORDER BY updated DESC LIMIT 5",
288 $ten_days_ago
289 );
290 $discussions = $this->wpdb->get_results( $query );
291 if ( empty( $discussions ) ) {
292 return;
293 }
294
295 foreach ( $discussions as $discussion ) {
296 $this->generate_title_for_discussion( $discussion );
297 }
298 }
299
300 /**
301 * UI route for listing discussions.
302 * Here we add the "forced cron" logic for up to 5 discussions,
303 * but only if auto-titling is enabled.
304 */
305 public function rest_discussions_ui_list( $request ) {
306 try {
307 $params = $request->get_json_params();
308 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
309 // Get paging setting from options
310 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
311 if ( $paging_option === 'None' ) {
312 $default_limit = 999; // Show all discussions
313 }
314 else {
315 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
316 }
317 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
318 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
319 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
320
321 if ( !is_null( $customId ) ) {
322 $botId = $customId;
323 }
324 if ( is_null( $botId ) ) {
325 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
326 }
327
328 $userId = get_current_user_id();
329 if ( !$userId ) {
330 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
331 }
332
333 $filters = [
334 [ 'accessor' => 'user', 'value' => $userId ],
335 [ 'accessor' => 'botId', 'value' => $botId ],
336 ];
337
338 // Retrieve the chats
339 $chats = $this->chats_query( [], $offset, $limit, $filters );
340
341 // NEW CHECK: only do forced titling if it's enabled
342 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
343 // "Forced cron" logic: check up to 5 that have no title
344 $counter = 0;
345 foreach ( $chats['rows'] as &$chatRow ) {
346 if ( $counter >= 5 ) {
347 break;
348 }
349 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
350 $discussionObj = (object) $chatRow;
351 $this->generate_title_for_discussion( $discussionObj );
352 $counter++;
353 }
354 }
355 // If you want the newly-updated titles to show up *immediately*:
356 $chats = $this->chats_query( [], $offset, $limit, $filters );
357 }
358 // END NEW CHECK
359
360 // Apply filters to discussion metadata
361 foreach ( $chats['rows'] as &$chatRow ) {
362 // Decode messages JSON to get the count
363 $messages = json_decode( $chatRow['messages'], true );
364 $message_count = is_array( $messages ) ? count( $messages ) : 0;
365
366 // Add formatted metadata that can be filtered
367 $chatRow['metadata_display'] = [
368 'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
369 'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
370 'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
371 ];
372 }
373
374 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
375 }
376 catch ( Exception $e ) {
377 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
378 }
379 }
380
381 public function rest_discussions_delete_admin( $request ) {
382 try {
383 $params = $request->get_json_params();
384 $chatsIds = $params['chatIds'];
385 if ( is_array( $chatsIds ) ) {
386 if ( count( $chatsIds ) === 0 ) {
387 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
388 }
389 foreach ( $chatsIds as $chatId ) {
390 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
391 }
392 }
393 return $this->create_rest_response( [ 'success' => true ], 200 );
394 }
395 catch ( Exception $e ) {
396 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
397 }
398 }
399
400 public function rest_discussions_delete( $request ) {
401 try {
402 $params = $request->get_json_params();
403 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
404
405 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
406 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
407 }
408
409 $userId = get_current_user_id();
410 if ( !$userId ) {
411 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
412 }
413
414 foreach ( $chatIds as $chatId ) {
415 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
416 }
417
418 return $this->create_rest_response( [ 'success' => true ], 200 );
419 }
420 catch ( Exception $e ) {
421 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
422 }
423 }
424
425 // Get latest discussion for the given parameter
426 public function get_discussion( $botId, $chatId ) {
427 $this->check_db();
428 $chat = $this->wpdb->get_row(
429 $this->wpdb->prepare(
430 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
431 $chatId,
432 $botId
433 ),
434 ARRAY_A
435 );
436 if ( $chat ) {
437 $chat['messages'] = json_decode( $chat['messages'] );
438 return $chat;
439 }
440 return null;
441 }
442
443 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
444 $this->check_db();
445 $offset = !empty( $offset ) ? intval( $offset ) : 0;
446 $limit = !empty( $limit ) ? intval( $limit ) : 5;
447 $filters = !empty( $filters ) ? $filters : [];
448 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
449
450 $where_clauses = [];
451 $where_values = [];
452
453 if ( is_array( $filters ) ) {
454 foreach ( $filters as $filter ) {
455 $value = $filter['value'];
456 if ( is_null( $value ) || $value === '' ) {
457 continue;
458 }
459 switch ( $filter['accessor'] ) {
460 case 'user':
461 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
462 if ( $isIP ) {
463 $where_clauses[] = 'ip = %s';
464 $where_values[] = $value;
465 }
466 else {
467 $where_clauses[] = 'userId = %d';
468 $where_values[] = intval( $value );
469 }
470 break;
471 case 'botId':
472 $where_clauses[] = 'botId = %s';
473 $where_values[] = $value;
474 break;
475 case 'preview':
476 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
477 $where_clauses[] = 'messages LIKE %s';
478 $where_values[] = $like;
479 break;
480 // Add other cases as needed
481 }
482 }
483 }
484
485 $where_sql = '';
486 if ( !empty( $where_clauses ) ) {
487 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
488 }
489 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
490
491 $limit_sql = '';
492 if ( $limit > 0 ) {
493 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
494 }
495
496 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
497 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
498
499 // Get the total count
500 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
501 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
502
503 return $chats;
504 }
505
506 public function chatbot_reply( $rawText, $query, $params, $extra ) {
507 global $mwai_core;
508 $userIp = $mwai_core->get_ip_address();
509 $userId = $mwai_core->get_user_id();
510 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
511 $chatId = $this->core->fix_chat_id( $query, $params );
512 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
513 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
514 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
515 $now = date( 'Y-m-d H:i:s' );
516
517 if ( !empty( $customId ) ) {
518 $botId = $customId;
519 }
520 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
521
522 // If there is a file for "Vision", add it to the message
523 if ( isset( $query->attachedFile ) && $query->attachedFile !== null ) {
524 $attachedFile = $query->attachedFile;
525 if ( $attachedFile->get_purpose() === 'vision' && $attachedFile->get_type() === 'url' ) {
526 $newMessage = "![Uploaded Image]({$attachedFile->get_url()})\n" . $newMessage;
527 }
528 }
529
530 $this->check_db();
531 $chat = $this->wpdb->get_row(
532 $this->wpdb->prepare(
533 "SELECT * FROM $this->table_chats WHERE chatId = %s",
534 $chatId
535 )
536 );
537 $messageExtra = [
538 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
539 ];
540 $chatExtra = [
541 'session' => $query->session,
542 'model' => $query->model,
543 ];
544 if ( !empty( $query->temperature ) ) {
545 $chatExtra['temperature'] = $query->temperature;
546 }
547 if ( !empty( $query->context ) ) {
548 $chatExtra['context'] = $query->context;
549 }
550 if ( !empty( $params['parentBotId'] ) ) {
551 $chatExtra['parentBotId'] = $params['parentBotId'];
552 }
553 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
554 $chatExtra['assistantId'] = $query->assistantId;
555 $chatExtra['threadId'] = $query->threadId;
556 $chatExtra['storeId'] = $query->storeId;
557 }
558
559 // Store response ID and date for Responses API
560 if ( !empty( $extra['responseId'] ) ) {
561 $chatExtra['previousResponseId'] = $extra['responseId'];
562 $chatExtra['previousResponseDate'] = $now;
563 }
564
565 if ( $chat ) {
566 $chat->messages = json_decode( $chat->messages );
567 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
568 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
569 $chat->messages = json_encode( $chat->messages );
570
571 // Update or merge extra data
572 $existingExtra = json_decode( $chat->extra, true ) ?: [];
573 $mergedExtra = array_merge( $existingExtra, $chatExtra );
574
575 $this->wpdb->update(
576 $this->table_chats,
577 [
578 'userId' => $userId,
579 'messages' => $chat->messages,
580 'extra' => json_encode( $mergedExtra ),
581 'updated' => $now
582 ],
583 [ 'id' => $chat->id ]
584 );
585 }
586 else {
587 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
588 $messages = [];
589 if ( !empty( $startSentence ) ) {
590 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
591 }
592 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
593 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
594 $chat = [
595 'userId' => $userId,
596 'ip' => $userIp,
597 'messages' => json_encode( $messages ),
598 'extra' => json_encode( $chatExtra ),
599 'botId' => $botId,
600 'chatId' => $chatId,
601 'threadId' => $threadId,
602 'storeId' => $storeId,
603 'created' => $now,
604 'updated' => $now
605 ];
606 $this->wpdb->insert( $this->table_chats, $chat );
607 }
608 return $rawText;
609 }
610
611 public function format_messages( $json, $format = 'html' ) {
612 $html = '';
613 if ( $format === 'html' ) {
614 try {
615 $conversation = json_decode( $json, true );
616 if ( json_last_error() !== JSON_ERROR_NONE ) {
617 return 'Invalid JSON format';
618 }
619 foreach ( $conversation as $message ) {
620 $role = ucfirst( $message['role'] );
621 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
622 }
623 }
624 catch ( Exception $e ) {
625 error_log( $e->getMessage() );
626 return 'Error while formatting the message';
627 }
628 }
629 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
630 return $html;
631 }
632
633 /**
634 * Commits a discussion into the database (create or update if the same chatId is found).
635 *
636 * @param Meow_MWAI_Discussion $discussionObject
637 * @return bool True if success, false if error
638 */
639 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
640 $this->check_db();
641
642 // 1. Check if a discussion with the same chatId already exists
643 $chat = $this->wpdb->get_row(
644 $this->wpdb->prepare(
645 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
646 $discussionObject->chatId
647 ),
648 ARRAY_A
649 );
650
651 // 2. Prepare data for DB
652 $userIp = $this->core->get_ip_address();
653 $userId = $this->core->get_user_id();
654 $now = date( 'Y-m-d H:i:s' );
655
656 $data = [
657 'userId' => $userId,
658 'ip' => $userIp,
659 'botId' => $discussionObject->botId,
660 'chatId' => $discussionObject->chatId,
661 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
662 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
663 'updated' => $now,
664 ];
665
666 // 3. Update if found, otherwise insert a new row
667 if ( $chat ) {
668 $updateRes = $this->wpdb->update(
669 $this->table_chats,
670 $data,
671 [ 'id' => $chat['id'] ]
672 );
673 if ( $updateRes === false ) {
674 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
675 return false;
676 }
677 }
678 else {
679 // For insertion, also set "created"
680 $data['created'] = $now;
681 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
682 if ( $insertRes === false ) {
683 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
684 return false;
685 }
686 }
687
688 return true;
689 }
690
691 public function check_db() {
692 if ( $this->db_check ) {
693 return true;
694 }
695 $this->db_check = !(
696 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
697 != strtolower( $this->table_chats )
698 );
699 if ( !$this->db_check ) {
700 $this->create_db();
701 $this->db_check = !(
702 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
703 != strtolower( $this->table_chats )
704 );
705 }
706
707 // LATER: REMOVE THIS AFTER MARCH 2025
708 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_chats LIKE 'title'" );
709 // if ( ! $this->db_check ) {
710 // $this->wpdb->query( "ALTER TABLE $this->table_chats ADD COLUMN title VARCHAR(64) NULL" );
711 // $this->db_check = true;
712 // }
713
714 // LATER: REMOVE THIS AFTER SEPTEMBER 2025
715 // Migrate guest users from userId = 0 to userId = NULL
716 $guest_count = $this->wpdb->get_var( "SELECT COUNT(*) FROM $this->table_chats WHERE userId = 0" );
717 if ( $guest_count > 0 ) {
718 $this->wpdb->query( "UPDATE $this->table_chats SET userId = NULL WHERE userId = 0" );
719 }
720
721 return $this->db_check;
722 }
723
724 public function create_db() {
725 $charset_collate = $this->wpdb->get_charset_collate();
726 $sqlLogs = "CREATE TABLE $this->table_chats (
727 id BIGINT(20) NOT NULL AUTO_INCREMENT,
728 userId BIGINT(20) NULL,
729 ip VARCHAR(64) NULL,
730 title VARCHAR(64) NULL,
731 messages TEXT NOT NULL NULL,
732 extra LONGTEXT NOT NULL NULL,
733 botId VARCHAR(64) NULL,
734 chatId VARCHAR(64) NOT NULL,
735 threadId VARCHAR(64) NULL,
736 storeId VARCHAR(64) NULL,
737 created DATETIME NOT NULL,
738 updated DATETIME NOT NULL,
739 PRIMARY KEY (id),
740 INDEX chatId (chatId)
741 ) $charset_collate;";
742 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
743 dbDelta( $sqlLogs );
744 }
745 }
746