PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.5
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.5
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 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
736 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 $now = date( 'Y-m-d H:i:s' );
274 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
275
276 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
277 $query = $this->wpdb->prepare(
278 "SELECT * FROM {$this->table_chats}
279 WHERE title IS NULL AND updated >= %s
280 ORDER BY updated DESC LIMIT 5",
281 $ten_days_ago
282 );
283 $discussions = $this->wpdb->get_results( $query );
284 if ( empty( $discussions ) ) {
285 return;
286 }
287
288 foreach ( $discussions as $discussion ) {
289 $this->generate_title_for_discussion( $discussion );
290 }
291 }
292
293 /**
294 * UI route for listing discussions.
295 * Here we add the "forced cron" logic for up to 5 discussions,
296 * but only if auto-titling is enabled.
297 */
298 public function rest_discussions_ui_list( $request ) {
299 try {
300 $params = $request->get_json_params();
301 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
302 // Get paging setting from options
303 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
304 if ( $paging_option === 'None' ) {
305 $default_limit = 999; // Show all discussions
306 }
307 else {
308 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
309 }
310 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
311 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
312 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
313
314 if ( !is_null( $customId ) ) {
315 $botId = $customId;
316 }
317 if ( is_null( $botId ) ) {
318 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
319 }
320
321 $userId = get_current_user_id();
322 if ( !$userId ) {
323 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
324 }
325
326 $filters = [
327 [ 'accessor' => 'user', 'value' => $userId ],
328 [ 'accessor' => 'botId', 'value' => $botId ],
329 ];
330
331 // Retrieve the chats
332 $chats = $this->chats_query( [], $offset, $limit, $filters );
333
334 // NEW CHECK: only do forced titling if it's enabled
335 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
336 // "Forced cron" logic: check up to 5 that have no title
337 $counter = 0;
338 foreach ( $chats['rows'] as &$chatRow ) {
339 if ( $counter >= 5 ) {
340 break;
341 }
342 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
343 $discussionObj = (object) $chatRow;
344 $this->generate_title_for_discussion( $discussionObj );
345 $counter++;
346 }
347 }
348 // If you want the newly-updated titles to show up *immediately*:
349 $chats = $this->chats_query( [], $offset, $limit, $filters );
350 }
351 // END NEW CHECK
352
353 // Apply filters to discussion metadata
354 foreach ( $chats['rows'] as &$chatRow ) {
355 // Decode messages JSON to get the count
356 $messages = json_decode( $chatRow['messages'], true );
357 $message_count = is_array( $messages ) ? count( $messages ) : 0;
358
359 // Add formatted metadata that can be filtered
360 $chatRow['metadata_display'] = [
361 'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
362 'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
363 'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
364 ];
365 }
366
367 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
368 }
369 catch ( Exception $e ) {
370 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
371 }
372 }
373
374 public function rest_discussions_delete_admin( $request ) {
375 try {
376 $params = $request->get_json_params();
377 $chatsIds = $params['chatIds'];
378 if ( is_array( $chatsIds ) ) {
379 if ( count( $chatsIds ) === 0 ) {
380 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
381 }
382 foreach ( $chatsIds as $chatId ) {
383 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
384 }
385 }
386 return $this->create_rest_response( [ 'success' => true ], 200 );
387 }
388 catch ( Exception $e ) {
389 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
390 }
391 }
392
393 public function rest_discussions_delete( $request ) {
394 try {
395 $params = $request->get_json_params();
396 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
397
398 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
399 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
400 }
401
402 $userId = get_current_user_id();
403 if ( !$userId ) {
404 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
405 }
406
407 foreach ( $chatIds as $chatId ) {
408 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
409 }
410
411 return $this->create_rest_response( [ 'success' => true ], 200 );
412 }
413 catch ( Exception $e ) {
414 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
415 }
416 }
417
418 // Get latest discussion for the given parameter
419 public function get_discussion( $botId, $chatId ) {
420 $this->check_db();
421 $chat = $this->wpdb->get_row(
422 $this->wpdb->prepare(
423 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
424 $chatId,
425 $botId
426 ),
427 ARRAY_A
428 );
429 if ( $chat ) {
430 $chat['messages'] = json_decode( $chat['messages'] );
431 return $chat;
432 }
433 return null;
434 }
435
436 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
437 $this->check_db();
438 $offset = !empty( $offset ) ? intval( $offset ) : 0;
439 $limit = !empty( $limit ) ? intval( $limit ) : 5;
440 $filters = !empty( $filters ) ? $filters : [];
441 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
442
443 $where_clauses = [];
444 $where_values = [];
445
446 if ( is_array( $filters ) ) {
447 foreach ( $filters as $filter ) {
448 $value = $filter['value'];
449 if ( is_null( $value ) || $value === '' ) {
450 continue;
451 }
452 switch ( $filter['accessor'] ) {
453 case 'user':
454 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
455 if ( $isIP ) {
456 $where_clauses[] = 'ip = %s';
457 $where_values[] = $value;
458 }
459 else {
460 $where_clauses[] = 'userId = %d';
461 $where_values[] = intval( $value );
462 }
463 break;
464 case 'botId':
465 $where_clauses[] = 'botId = %s';
466 $where_values[] = $value;
467 break;
468 case 'preview':
469 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
470 $where_clauses[] = 'messages LIKE %s';
471 $where_values[] = $like;
472 break;
473 // Add other cases as needed
474 }
475 }
476 }
477
478 $where_sql = '';
479 if ( !empty( $where_clauses ) ) {
480 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
481 }
482 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
483
484 $limit_sql = '';
485 if ( $limit > 0 ) {
486 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
487 }
488
489 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
490 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
491
492 // Get the total count
493 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
494 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
495
496 return $chats;
497 }
498
499 public function chatbot_reply( $rawText, $query, $params, $extra ) {
500 global $mwai_core;
501 $userIp = $mwai_core->get_ip_address();
502 $userId = $mwai_core->get_user_id();
503 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
504 $chatId = $this->core->fix_chat_id( $query, $params );
505 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
506 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
507 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
508 $now = date( 'Y-m-d H:i:s' );
509
510 if ( !empty( $customId ) ) {
511 $botId = $customId;
512 }
513 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
514
515 // If there is a file for "Vision", add it to the message
516 if ( isset( $query->filePurpose ) && $query->filePurpose === 'vision' && isset( $query->file ) ) {
517 $newMessage = "![Uploaded Image]({$query->file})\n" . $newMessage;
518 }
519
520 $this->check_db();
521 $chat = $this->wpdb->get_row(
522 $this->wpdb->prepare(
523 "SELECT * FROM $this->table_chats WHERE chatId = %s",
524 $chatId
525 )
526 );
527 $messageExtra = [
528 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
529 ];
530 $chatExtra = [
531 'session' => $query->session,
532 'model' => $query->model,
533 ];
534 if ( !empty( $query->temperature ) ) {
535 $chatExtra['temperature'] = $query->temperature;
536 }
537 if ( !empty( $query->context ) ) {
538 $chatExtra['context'] = $query->context;
539 }
540 if ( !empty( $params['parentBotId'] ) ) {
541 $chatExtra['parentBotId'] = $params['parentBotId'];
542 }
543 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
544 $chatExtra['assistantId'] = $query->assistantId;
545 $chatExtra['threadId'] = $query->threadId;
546 $chatExtra['storeId'] = $query->storeId;
547 }
548
549 // Store response ID and date for Responses API
550 if ( !empty( $extra['responseId'] ) ) {
551 $chatExtra['previousResponseId'] = $extra['responseId'];
552 $chatExtra['previousResponseDate'] = $now;
553 }
554
555 if ( $chat ) {
556 $chat->messages = json_decode( $chat->messages );
557 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
558 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
559 $chat->messages = json_encode( $chat->messages );
560
561 // Update or merge extra data
562 $existingExtra = json_decode( $chat->extra, true ) ?: [];
563 $mergedExtra = array_merge( $existingExtra, $chatExtra );
564
565 $this->wpdb->update(
566 $this->table_chats,
567 [
568 'userId' => $userId,
569 'messages' => $chat->messages,
570 'extra' => json_encode( $mergedExtra ),
571 'updated' => $now
572 ],
573 [ 'id' => $chat->id ]
574 );
575 }
576 else {
577 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
578 $messages = [];
579 if ( !empty( $startSentence ) ) {
580 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
581 }
582 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
583 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
584 $chat = [
585 'userId' => $userId,
586 'ip' => $userIp,
587 'messages' => json_encode( $messages ),
588 'extra' => json_encode( $chatExtra ),
589 'botId' => $botId,
590 'chatId' => $chatId,
591 'threadId' => $threadId,
592 'storeId' => $storeId,
593 'created' => $now,
594 'updated' => $now
595 ];
596 $this->wpdb->insert( $this->table_chats, $chat );
597 }
598 return $rawText;
599 }
600
601 public function format_messages( $json, $format = 'html' ) {
602 $html = '';
603 if ( $format === 'html' ) {
604 try {
605 $conversation = json_decode( $json, true );
606 if ( json_last_error() !== JSON_ERROR_NONE ) {
607 return 'Invalid JSON format';
608 }
609 foreach ( $conversation as $message ) {
610 $role = ucfirst( $message['role'] );
611 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
612 }
613 }
614 catch ( Exception $e ) {
615 error_log( $e->getMessage() );
616 return 'Error while formatting the message';
617 }
618 }
619 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
620 return $html;
621 }
622
623 /**
624 * Commits a discussion into the database (create or update if the same chatId is found).
625 *
626 * @param Meow_MWAI_Discussion $discussionObject
627 * @return bool True if success, false if error
628 */
629 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
630 $this->check_db();
631
632 // 1. Check if a discussion with the same chatId already exists
633 $chat = $this->wpdb->get_row(
634 $this->wpdb->prepare(
635 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
636 $discussionObject->chatId
637 ),
638 ARRAY_A
639 );
640
641 // 2. Prepare data for DB
642 $userIp = $this->core->get_ip_address();
643 $userId = $this->core->get_user_id();
644 $now = date( 'Y-m-d H:i:s' );
645
646 $data = [
647 'userId' => $userId,
648 'ip' => $userIp,
649 'botId' => $discussionObject->botId,
650 'chatId' => $discussionObject->chatId,
651 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
652 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
653 'updated' => $now,
654 ];
655
656 // 3. Update if found, otherwise insert a new row
657 if ( $chat ) {
658 $updateRes = $this->wpdb->update(
659 $this->table_chats,
660 $data,
661 [ 'id' => $chat['id'] ]
662 );
663 if ( $updateRes === false ) {
664 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
665 return false;
666 }
667 }
668 else {
669 // For insertion, also set "created"
670 $data['created'] = $now;
671 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
672 if ( $insertRes === false ) {
673 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
674 return false;
675 }
676 }
677
678 return true;
679 }
680
681 public function check_db() {
682 if ( $this->db_check ) {
683 return true;
684 }
685 $this->db_check = !(
686 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
687 != strtolower( $this->table_chats )
688 );
689 if ( !$this->db_check ) {
690 $this->create_db();
691 $this->db_check = !(
692 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
693 != strtolower( $this->table_chats )
694 );
695 }
696
697 // LATER: REMOVE THIS AFTER MARCH 2025
698 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_chats LIKE 'title'" );
699 // if ( ! $this->db_check ) {
700 // $this->wpdb->query( "ALTER TABLE $this->table_chats ADD COLUMN title VARCHAR(64) NULL" );
701 // $this->db_check = true;
702 // }
703
704 // LATER: REMOVE THIS AFTER SEPTEMBER 2025
705 // Migrate guest users from userId = 0 to userId = NULL
706 $guest_count = $this->wpdb->get_var( "SELECT COUNT(*) FROM $this->table_chats WHERE userId = 0" );
707 if ( $guest_count > 0 ) {
708 $this->wpdb->query( "UPDATE $this->table_chats SET userId = NULL WHERE userId = 0" );
709 }
710
711 return $this->db_check;
712 }
713
714 public function create_db() {
715 $charset_collate = $this->wpdb->get_charset_collate();
716 $sqlLogs = "CREATE TABLE $this->table_chats (
717 id BIGINT(20) NOT NULL AUTO_INCREMENT,
718 userId BIGINT(20) NULL,
719 ip VARCHAR(64) NULL,
720 title VARCHAR(64) NULL,
721 messages TEXT NOT NULL NULL,
722 extra LONGTEXT NOT NULL NULL,
723 botId VARCHAR(64) NULL,
724 chatId VARCHAR(64) NOT NULL,
725 threadId VARCHAR(64) NULL,
726 storeId VARCHAR(64) NULL,
727 created DATETIME NOT NULL,
728 updated DATETIME NOT NULL,
729 PRIMARY KEY (id),
730 INDEX chatId (chatId)
731 ) $charset_collate;";
732 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
733 dbDelta( $sqlLogs );
734 }
735 }
736