PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.2
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 1 year ago gdpr.php 1 year ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 1 year ago
discussions.php
747 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 if ( $this->core->get_option( 'debug_mode' ) ) {
110 error_log( '[MWAI] Token refresh: Nonce is 12-24 hours old, providing fresh token' );
111 }
112 }
113 }
114
115 // If the nonce has changed or should be refreshed, include the new one
116 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
117 $data['new_token'] = $current_nonce;
118
119 // Log if debug mode is enabled
120 if ( $this->core->get_option( 'debug_mode' ) ) {
121 error_log( '[MWAI] Token refresh: Sending new token in response' );
122 }
123 }
124
125 return new WP_REST_Response( $data, $status );
126 }
127
128 /**
129 * Generate or update the title for a specific discussion
130 * by calling the AI (if it meets the requirements).
131 *
132 * @param stdClass $discussion A row from the DB (object form).
133 * @return void
134 */
135 private function generate_title_for_discussion( $discussion ) {
136 // Check if there's already a title
137 if ( !empty( $discussion->title ) ) {
138 return; // Nothing to do if title is already set.
139 }
140
141 // Ensure it's not older than 10 days, or whatever logic you prefer
142 $ten_days_ago = strtotime( '-10 days' );
143 if ( strtotime( $discussion->updated ) < $ten_days_ago ) {
144 return; // Skip if older than 10 days
145 }
146
147 // We expect JSON in the messages
148 $messages = json_decode( $discussion->messages, true );
149 if ( !is_array( $messages ) ) {
150 return;
151 }
152
153 // Check for at least one user and one assistant message
154 $has_user_message = false;
155 $has_assistant_message = false;
156 foreach ( $messages as $message ) {
157 if ( isset( $message['role'] ) ) {
158 if ( $message['role'] === 'user' ) {
159 $has_user_message = true;
160 }
161 if ( $message['role'] === 'assistant' ) {
162 $has_assistant_message = true;
163 }
164 }
165 if ( $has_user_message && $has_assistant_message ) {
166 break;
167 }
168 }
169
170 if ( !( $has_user_message && $has_assistant_message ) ) {
171 return; // If doesn't have both, skip
172 }
173
174 // Prepare the conversation text for the prompt
175 $conversation_text = '';
176 foreach ( $messages as $message ) {
177 if ( isset( $message['role'] ) && isset( $message['content'] ) ) {
178 $role = ucfirst( $message['role'] );
179 $content = $message['content'];
180 $conversation_text .= "$role: $content\n";
181 }
182 }
183
184 $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";
185 $prompt = apply_filters( 'mwai_discussions_title_prompt', $base_prompt, $conversation_text, $discussion );
186
187 // Run the AI query using the fast environment
188 global $mwai;
189 $params = [ 'scope' => 'discussions' ];
190
191 $fastEnv = $this->core->get_option( 'ai_fast_default_env' );
192 $fastModel = $this->core->get_option( 'ai_fast_default_model' );
193
194 if ( !empty( $fastEnv ) ) {
195 $params['envId'] = $fastEnv;
196 }
197 if ( !empty( $fastModel ) ) {
198 $params['model'] = $fastModel;
199 }
200
201 $answer = $mwai->simpleTextQuery( $prompt, $params );
202
203 // Clean up the answer
204 $title = trim( $answer );
205 $title = rtrim( $title, '.!?:;,—–-–' ); // Remove trailing punctuation
206 $title = substr( $title, 0, 64 ); // Ensure less than 64 characters
207 if ( empty( $title ) ) {
208 $title = 'Untitled';
209 }
210
211 // Update the discussion with the title
212 $updated = $this->wpdb->update(
213 $this->table_chats,
214 [ 'title' => $title ],
215 [ 'id' => $discussion->id ]
216 );
217 if ( $updated === false ) {
218 error_log( "Failed to update the title for discussion ID {$discussion->id}" );
219 }
220 }
221
222 /**
223 * Admin route for listing discussions. No forced logic here.
224 */
225 public function rest_discussions_list( $request ) {
226 try {
227 $params = $request->get_json_params();
228 $offset = $params['offset'];
229 $limit = $params['limit'];
230 $filters = $params['filters'];
231 $sort = $params['sort'];
232
233 // Retrieve the chats
234 $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );
235
236 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
237 }
238 catch ( Exception $e ) {
239 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
240 }
241 }
242
243 public function rest_discussions_ui_edit( $request ) {
244 try {
245 $params = $request->get_json_params();
246 $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
247 $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;
248
249 if ( is_null( $chatId ) || is_null( $title ) ) {
250 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
251 }
252
253 $userId = get_current_user_id();
254 if ( !$userId ) {
255 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
256 }
257
258 // Update the discussion title for the current user
259 $updated = $this->wpdb->update(
260 $this->table_chats,
261 [ 'title' => $title ],
262 [ 'chatId' => $chatId, 'userId' => $userId ]
263 );
264 if ( $updated === false ) {
265 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
266 }
267
268 return $this->create_rest_response( [ 'success' => true ], 200 );
269 }
270 catch ( Exception $e ) {
271 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
272 }
273 }
274
275 public function cron_discussions() {
276 $this->check_db();
277
278 // NEW CHECK: Only run if auto-titling is enabled
279 if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
280 return;
281 }
282 // END NEW CHECK
283
284 $now = date( 'Y-m-d H:i:s' );
285 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
286
287 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
288 $query = $this->wpdb->prepare(
289 "SELECT * FROM {$this->table_chats}
290 WHERE title IS NULL AND updated >= %s
291 ORDER BY updated DESC LIMIT 5",
292 $ten_days_ago
293 );
294 $discussions = $this->wpdb->get_results( $query );
295 if ( empty( $discussions ) ) {
296 return;
297 }
298
299 foreach ( $discussions as $discussion ) {
300 $this->generate_title_for_discussion( $discussion );
301 }
302 }
303
304 /**
305 * UI route for listing discussions.
306 * Here we add the "forced cron" logic for up to 5 discussions,
307 * but only if auto-titling is enabled.
308 */
309 public function rest_discussions_ui_list( $request ) {
310 try {
311 $params = $request->get_json_params();
312 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
313 // Get paging setting from options
314 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
315 if ( $paging_option === 'None' ) {
316 $default_limit = 999; // Show all discussions
317 }
318 else {
319 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
320 }
321 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
322 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
323 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
324
325 if ( !is_null( $customId ) ) {
326 $botId = $customId;
327 }
328 if ( is_null( $botId ) ) {
329 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
330 }
331
332 $userId = get_current_user_id();
333 if ( !$userId ) {
334 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
335 }
336
337 $filters = [
338 [ 'accessor' => 'user', 'value' => $userId ],
339 [ 'accessor' => 'botId', 'value' => $botId ],
340 ];
341
342 // Retrieve the chats
343 $chats = $this->chats_query( [], $offset, $limit, $filters );
344
345 // NEW CHECK: only do forced titling if it's enabled
346 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
347 // "Forced cron" logic: check up to 5 that have no title
348 $counter = 0;
349 foreach ( $chats['rows'] as &$chatRow ) {
350 if ( $counter >= 5 ) {
351 break;
352 }
353 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
354 $discussionObj = (object) $chatRow;
355 $this->generate_title_for_discussion( $discussionObj );
356 $counter++;
357 }
358 }
359 // If you want the newly-updated titles to show up *immediately*:
360 $chats = $this->chats_query( [], $offset, $limit, $filters );
361 }
362 // END NEW CHECK
363
364 // Apply filters to discussion metadata
365 foreach ( $chats['rows'] as &$chatRow ) {
366 // Decode messages JSON to get the count
367 $messages = json_decode( $chatRow['messages'], true );
368 $message_count = is_array( $messages ) ? count( $messages ) : 0;
369
370 // Add formatted metadata that can be filtered
371 $chatRow['metadata_display'] = [
372 'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
373 'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
374 'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
375 ];
376 }
377
378 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
379 }
380 catch ( Exception $e ) {
381 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
382 }
383 }
384
385 public function rest_discussions_delete_admin( $request ) {
386 try {
387 $params = $request->get_json_params();
388 $chatsIds = $params['chatIds'];
389 if ( is_array( $chatsIds ) ) {
390 if ( count( $chatsIds ) === 0 ) {
391 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
392 }
393 foreach ( $chatsIds as $chatId ) {
394 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
395 }
396 }
397 return $this->create_rest_response( [ 'success' => true ], 200 );
398 }
399 catch ( Exception $e ) {
400 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
401 }
402 }
403
404 public function rest_discussions_delete( $request ) {
405 try {
406 $params = $request->get_json_params();
407 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
408
409 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
410 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
411 }
412
413 $userId = get_current_user_id();
414 if ( !$userId ) {
415 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
416 }
417
418 foreach ( $chatIds as $chatId ) {
419 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
420 }
421
422 return $this->create_rest_response( [ 'success' => true ], 200 );
423 }
424 catch ( Exception $e ) {
425 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
426 }
427 }
428
429 // Get latest discussion for the given parameter
430 public function get_discussion( $botId, $chatId ) {
431 $this->check_db();
432 $chat = $this->wpdb->get_row(
433 $this->wpdb->prepare(
434 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
435 $chatId,
436 $botId
437 ),
438 ARRAY_A
439 );
440 if ( $chat ) {
441 $chat['messages'] = json_decode( $chat['messages'] );
442 return $chat;
443 }
444 return null;
445 }
446
447 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
448 $this->check_db();
449 $offset = !empty( $offset ) ? intval( $offset ) : 0;
450 $limit = !empty( $limit ) ? intval( $limit ) : 5;
451 $filters = !empty( $filters ) ? $filters : [];
452 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
453
454 $where_clauses = [];
455 $where_values = [];
456
457 if ( is_array( $filters ) ) {
458 foreach ( $filters as $filter ) {
459 $value = $filter['value'];
460 if ( is_null( $value ) || $value === '' ) {
461 continue;
462 }
463 switch ( $filter['accessor'] ) {
464 case 'user':
465 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
466 if ( $isIP ) {
467 $where_clauses[] = 'ip = %s';
468 $where_values[] = $value;
469 }
470 else {
471 $where_clauses[] = 'userId = %d';
472 $where_values[] = intval( $value );
473 }
474 break;
475 case 'botId':
476 $where_clauses[] = 'botId = %s';
477 $where_values[] = $value;
478 break;
479 case 'preview':
480 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
481 $where_clauses[] = 'messages LIKE %s';
482 $where_values[] = $like;
483 break;
484 // Add other cases as needed
485 }
486 }
487 }
488
489 $where_sql = '';
490 if ( !empty( $where_clauses ) ) {
491 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
492 }
493 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
494
495 $limit_sql = '';
496 if ( $limit > 0 ) {
497 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
498 }
499
500 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
501 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
502
503 // Get the total count
504 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
505 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
506
507 return $chats;
508 }
509
510 public function chatbot_reply( $rawText, $query, $params, $extra ) {
511 global $mwai_core;
512 $userIp = $mwai_core->get_ip_address();
513 $userId = $mwai_core->get_user_id();
514 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
515 $chatId = $this->core->fix_chat_id( $query, $params );
516 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
517 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
518 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
519 $now = date( 'Y-m-d H:i:s' );
520
521 if ( !empty( $customId ) ) {
522 $botId = $customId;
523 }
524 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
525
526 // If there is a file for "Vision", add it to the message
527 if ( isset( $query->filePurpose ) && $query->filePurpose === 'vision' && isset( $query->file ) ) {
528 $newMessage = "![Uploaded Image]({$query->file})\n" . $newMessage;
529 }
530
531 $this->check_db();
532 $chat = $this->wpdb->get_row(
533 $this->wpdb->prepare(
534 "SELECT * FROM $this->table_chats WHERE chatId = %s",
535 $chatId
536 )
537 );
538 $messageExtra = [
539 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
540 ];
541 $chatExtra = [
542 'session' => $query->session,
543 'model' => $query->model,
544 ];
545 if ( !empty( $query->temperature ) ) {
546 $chatExtra['temperature'] = $query->temperature;
547 }
548 if ( !empty( $query->context ) ) {
549 $chatExtra['context'] = $query->context;
550 }
551 if ( !empty( $params['parentBotId'] ) ) {
552 $chatExtra['parentBotId'] = $params['parentBotId'];
553 }
554 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
555 $chatExtra['assistantId'] = $query->assistantId;
556 $chatExtra['threadId'] = $query->threadId;
557 $chatExtra['storeId'] = $query->storeId;
558 }
559
560 // Store response ID and date for Responses API
561 if ( !empty( $extra['responseId'] ) ) {
562 $chatExtra['previousResponseId'] = $extra['responseId'];
563 $chatExtra['previousResponseDate'] = $now;
564 }
565
566 if ( $chat ) {
567 $chat->messages = json_decode( $chat->messages );
568 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
569 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
570 $chat->messages = json_encode( $chat->messages );
571
572 // Update or merge extra data
573 $existingExtra = json_decode( $chat->extra, true ) ?: [];
574 $mergedExtra = array_merge( $existingExtra, $chatExtra );
575
576 $this->wpdb->update(
577 $this->table_chats,
578 [
579 'userId' => $userId,
580 'messages' => $chat->messages,
581 'extra' => json_encode( $mergedExtra ),
582 'updated' => $now
583 ],
584 [ 'id' => $chat->id ]
585 );
586 }
587 else {
588 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
589 $messages = [];
590 if ( !empty( $startSentence ) ) {
591 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
592 }
593 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
594 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
595 $chat = [
596 'userId' => $userId,
597 'ip' => $userIp,
598 'messages' => json_encode( $messages ),
599 'extra' => json_encode( $chatExtra ),
600 'botId' => $botId,
601 'chatId' => $chatId,
602 'threadId' => $threadId,
603 'storeId' => $storeId,
604 'created' => $now,
605 'updated' => $now
606 ];
607 $this->wpdb->insert( $this->table_chats, $chat );
608 }
609 return $rawText;
610 }
611
612 public function format_messages( $json, $format = 'html' ) {
613 $html = '';
614 if ( $format === 'html' ) {
615 try {
616 $conversation = json_decode( $json, true );
617 if ( json_last_error() !== JSON_ERROR_NONE ) {
618 return 'Invalid JSON format';
619 }
620 foreach ( $conversation as $message ) {
621 $role = ucfirst( $message['role'] );
622 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
623 }
624 }
625 catch ( Exception $e ) {
626 error_log( $e->getMessage() );
627 return 'Error while formatting the message';
628 }
629 }
630 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
631 return $html;
632 }
633
634 /**
635 * Commits a discussion into the database (create or update if the same chatId is found).
636 *
637 * @param Meow_MWAI_Discussion $discussionObject
638 * @return bool True if success, false if error
639 */
640 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
641 $this->check_db();
642
643 // 1. Check if a discussion with the same chatId already exists
644 $chat = $this->wpdb->get_row(
645 $this->wpdb->prepare(
646 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
647 $discussionObject->chatId
648 ),
649 ARRAY_A
650 );
651
652 // 2. Prepare data for DB
653 $userIp = $this->core->get_ip_address();
654 $userId = $this->core->get_user_id();
655 $now = date( 'Y-m-d H:i:s' );
656
657 $data = [
658 'userId' => $userId,
659 'ip' => $userIp,
660 'botId' => $discussionObject->botId,
661 'chatId' => $discussionObject->chatId,
662 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
663 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
664 'updated' => $now,
665 ];
666
667 // 3. Update if found, otherwise insert a new row
668 if ( $chat ) {
669 $updateRes = $this->wpdb->update(
670 $this->table_chats,
671 $data,
672 [ 'id' => $chat['id'] ]
673 );
674 if ( $updateRes === false ) {
675 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
676 return false;
677 }
678 }
679 else {
680 // For insertion, also set "created"
681 $data['created'] = $now;
682 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
683 if ( $insertRes === false ) {
684 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
685 return false;
686 }
687 }
688
689 return true;
690 }
691
692 public function check_db() {
693 if ( $this->db_check ) {
694 return true;
695 }
696 $this->db_check = !(
697 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
698 != strtolower( $this->table_chats )
699 );
700 if ( !$this->db_check ) {
701 $this->create_db();
702 $this->db_check = !(
703 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
704 != strtolower( $this->table_chats )
705 );
706 }
707
708 // LATER: REMOVE THIS AFTER MARCH 2025
709 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_chats LIKE 'title'" );
710 // if ( ! $this->db_check ) {
711 // $this->wpdb->query( "ALTER TABLE $this->table_chats ADD COLUMN title VARCHAR(64) NULL" );
712 // $this->db_check = true;
713 // }
714
715 // LATER: REMOVE THIS AFTER SEPTEMBER 2025
716 // Migrate guest users from userId = 0 to userId = NULL
717 $guest_count = $this->wpdb->get_var( "SELECT COUNT(*) FROM $this->table_chats WHERE userId = 0" );
718 if ( $guest_count > 0 ) {
719 $this->wpdb->query( "UPDATE $this->table_chats SET userId = NULL WHERE userId = 0" );
720 }
721
722 return $this->db_check;
723 }
724
725 public function create_db() {
726 $charset_collate = $this->wpdb->get_charset_collate();
727 $sqlLogs = "CREATE TABLE $this->table_chats (
728 id BIGINT(20) NOT NULL AUTO_INCREMENT,
729 userId BIGINT(20) NULL,
730 ip VARCHAR(64) NULL,
731 title VARCHAR(64) NULL,
732 messages TEXT NOT NULL NULL,
733 extra LONGTEXT NOT NULL NULL,
734 botId VARCHAR(64) NULL,
735 chatId VARCHAR(64) NOT NULL,
736 threadId VARCHAR(64) NULL,
737 storeId VARCHAR(64) NULL,
738 created DATETIME NOT NULL,
739 updated DATETIME NOT NULL,
740 PRIMARY KEY (id),
741 INDEX chatId (chatId)
742 ) $charset_collate;";
743 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
744 dbDelta( $sqlLogs );
745 }
746 }
747