PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.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 7 months ago chatbot.php 7 months ago discussions.php 8 months ago files.php 7 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 11 months ago security.php 11 months ago tasks-examples.php 9 months ago tasks.php 7 months ago wand.php 7 months ago
discussions.php
852 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 // TODO: Remove after January 2026 - Legacy cron support
23 // Old cron scheduling removed - now handled by Tasks module
24 // if ( !wp_next_scheduled( 'mwai_discussions' ) ) {
25 // wp_schedule_event( time(), 'hourly', 'mwai_discussions' );
26 // }
27 // add_action( 'mwai_discussions', [ $this, 'cron_discussions' ] );
28
29 // Register task handler
30 add_filter( 'mwai_task_cleanup_discussions', [ $this, 'handle_cleanup_task' ], 10, 2 );
31 }
32 }
33
34 public function rest_api_init() {
35 // Admin
36 register_rest_route( $this->namespace_admin, '/discussions/list', [
37 'methods' => 'POST',
38 'callback' => [ $this, 'rest_discussions_list' ],
39 'permission_callback' => [ $this->core, 'can_access_settings' ],
40 ] );
41 register_rest_route( $this->namespace_admin, '/discussions/delete', [
42 'methods' => 'POST',
43 'callback' => [ $this, 'rest_discussions_delete_admin' ],
44 'permission_callback' => [ $this->core, 'can_access_settings' ],
45 ] );
46
47 // UI
48 register_rest_route( $this->namespace_ui, '/discussions/list', [
49 'methods' => 'POST',
50 'callback' => [ $this, 'rest_discussions_ui_list' ],
51 'permission_callback' => '__return_true'
52 ] );
53 register_rest_route( $this->namespace_ui, '/discussions/edit', [
54 'methods' => 'POST',
55 'callback' => [ $this, 'rest_discussions_ui_edit' ],
56 'permission_callback' => '__return_true'
57 ] );
58 register_rest_route( $this->namespace_ui, '/discussions/delete', [
59 'methods' => 'POST',
60 'callback' => [ $this, 'rest_discussions_delete' ],
61 'permission_callback' => [ $this, 'can_delete_discussion' ],
62 ] );
63 }
64
65 public function can_delete_discussion( $request ) {
66 $params = $request->get_json_params();
67 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
68 $userId = get_current_user_id();
69 if ( !$userId ) {
70 return false;
71 }
72 foreach ( $chatIds as $chatId ) {
73 $chat = $this->wpdb->get_row(
74 $this->wpdb->prepare(
75 "SELECT * FROM $this->table_chats WHERE chatId = %s",
76 $chatId
77 )
78 );
79 if ( !$chat || (int) $chat->userId !== (int) $userId ) {
80 return false;
81 }
82 }
83 return true;
84 }
85
86 /**
87 * Helper method to create REST responses with automatic token refresh
88 *
89 * @param array $data The response data
90 * @param int $status HTTP status code
91 * @return WP_REST_Response
92 */
93 protected function create_rest_response( $data, $status = 200 ) {
94 // Always check if we need to provide a new nonce
95 $current_nonce = $this->core->get_nonce( true );
96 $request_nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? $_SERVER['HTTP_X_WP_NONCE'] : null;
97
98 // Check if nonce is approaching expiration (WordPress nonces last 12-24 hours)
99 // We'll refresh if the nonce is older than 10 hours to be safe
100 $should_refresh = false;
101
102 if ( $request_nonce ) {
103 // Try to determine the age of the nonce
104 // WordPress uses a tick system where each tick is 12 hours
105 // If we're in the second half of the nonce's life, refresh it
106 $time = time();
107 $nonce_tick = wp_nonce_tick();
108
109 // Verify if the nonce is still valid but getting old
110 $verify = wp_verify_nonce( $request_nonce, 'wp_rest' );
111 if ( $verify === 2 ) {
112 // Nonce is valid but was generated 12-24 hours ago
113 $should_refresh = true;
114 // Log will be written when token is included in response
115 }
116 }
117
118 // If the nonce has changed or should be refreshed, include the new one
119 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
120 $data['new_token'] = $current_nonce;
121
122 // Log if server debug mode is enabled
123 if ( $this->core->get_option( 'server_debug_mode' ) ) {
124 error_log( '[AI Engine] Token refresh: Nonce refreshed (12-24 hours old)' );
125 }
126 }
127
128 return new WP_REST_Response( $data, $status );
129 }
130
131 /**
132 * Generate or update the title for a specific discussion
133 * by calling the AI (if it meets the requirements).
134 *
135 * @param stdClass $discussion A row from the DB (object form).
136 * @return void
137 */
138 private function generate_title_for_discussion( $discussion ) {
139 // Check if there's already a title
140 if ( !empty( $discussion->title ) ) {
141 return; // Nothing to do if title is already set.
142 }
143
144 // Ensure it's not older than 10 days, or whatever logic you prefer
145 $ten_days_ago = strtotime( '-10 days' );
146 if ( strtotime( $discussion->updated ) < $ten_days_ago ) {
147 return; // Skip if older than 10 days
148 }
149
150 // We expect JSON in the messages
151 $messages = json_decode( $discussion->messages, true );
152 if ( !is_array( $messages ) ) {
153 return;
154 }
155
156 // Check for at least one user and one assistant message
157 $has_user_message = false;
158 $has_assistant_message = false;
159 foreach ( $messages as $message ) {
160 if ( isset( $message['role'] ) ) {
161 if ( $message['role'] === 'user' ) {
162 $has_user_message = true;
163 }
164 if ( $message['role'] === 'assistant' ) {
165 $has_assistant_message = true;
166 }
167 }
168 if ( $has_user_message && $has_assistant_message ) {
169 break;
170 }
171 }
172
173 if ( !( $has_user_message && $has_assistant_message ) ) {
174 return; // If doesn't have both, skip
175 }
176
177 // Prepare the conversation text for the prompt
178 $conversation_text = '';
179 foreach ( $messages as $message ) {
180 if ( isset( $message['role'] ) && isset( $message['content'] ) ) {
181 $role = ucfirst( $message['role'] );
182 $content = $message['content'];
183 $conversation_text .= "$role: $content\n";
184 }
185 }
186
187 $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.", 'ai-engine' ) . "\n\n" . __( 'Conversation:', 'ai-engine' ) . "\n$conversation_text\n";
188 $prompt = apply_filters( 'mwai_discussions_title_prompt', $base_prompt, $conversation_text, $discussion );
189
190 // Run the AI query using the fast environment
191 global $mwai;
192 $params = [ 'scope' => 'discussions' ];
193
194 // Use simpleFastTextQuery which handles Fast Model configuration
195 try {
196 $answer = $mwai->simpleFastTextQuery( $prompt, $params );
197
198 // Clean up the answer
199 $title = trim( $answer );
200 $title = rtrim( $title, '.!?:;,—–-–' ); // Remove trailing punctuation
201 $title = substr( $title, 0, 64 ); // Ensure less than 64 characters
202 if ( empty( $title ) ) {
203 $title = __( 'Untitled', 'ai-engine' );
204 }
205 } catch ( Exception $e ) {
206 // Handle content filter or other API errors
207 $error_message = $e->getMessage();
208 if ( strpos( $error_message, 'content_filter' ) !== false ||
209 strpos( $error_message, 'ResponsibleAIPolicyViolation' ) !== false ) {
210 error_log( "AI Engine: Content filter blocked title generation for discussion ID {$discussion->id}. Using fallback title." );
211 $title = __( 'Discussion', 'ai-engine' ) . ' ' . date( 'Y-m-d H:i' );
212 } else {
213 error_log( "AI Engine: Failed to generate title for discussion ID {$discussion->id}: " . $error_message );
214 $title = __( 'Untitled', 'ai-engine' );
215 }
216 }
217
218 // Update the discussion with the title
219 $updated = $this->wpdb->update(
220 $this->table_chats,
221 [ 'title' => $title ],
222 [ 'id' => $discussion->id ]
223 );
224 if ( $updated === false ) {
225 error_log( "Failed to update the title for discussion ID {$discussion->id}" );
226 }
227 }
228
229 /**
230 * Admin route for listing discussions. No forced logic here.
231 */
232 public function rest_discussions_list( $request ) {
233 try {
234 $params = $request->get_json_params();
235 $offset = $params['offset'];
236 $limit = $params['limit'];
237 $filters = $params['filters'];
238 $sort = $params['sort'];
239
240 // Retrieve the chats
241 $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );
242
243 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
244 }
245 catch ( Exception $e ) {
246 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
247 }
248 }
249
250 public function rest_discussions_ui_edit( $request ) {
251 try {
252 $params = $request->get_json_params();
253 $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
254 $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;
255
256 if ( is_null( $chatId ) || is_null( $title ) ) {
257 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
258 }
259
260 $userId = get_current_user_id();
261 if ( !$userId ) {
262 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
263 }
264
265 // Update the discussion title for the current user
266 $updated = $this->wpdb->update(
267 $this->table_chats,
268 [ 'title' => $title ],
269 [ 'chatId' => $chatId, 'userId' => $userId ]
270 );
271 if ( $updated === false ) {
272 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
273 }
274
275 return $this->create_rest_response( [ 'success' => true ], 200 );
276 }
277 catch ( Exception $e ) {
278 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
279 }
280 }
281
282 public function cron_discussions() {
283 // Track cron execution start
284 $this->core->track_cron_start( 'mwai_discussions' );
285
286 try {
287 $this->check_db();
288
289 // NEW CHECK: Only run if auto-titling is enabled
290 if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
291 $this->core->track_cron_end( 'mwai_discussions', 'success' );
292 return;
293 }
294 // END NEW CHECK
295
296 // Set the current user to the first admin to avoid guest limits
297 $admin_users = get_users( array( 'role' => 'administrator', 'number' => 1 ) );
298 if ( ! empty( $admin_users ) ) {
299 $admin_user = $admin_users[0];
300 wp_set_current_user( $admin_user->ID );
301 }
302
303 $now = date( 'Y-m-d H:i:s' );
304 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
305
306 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
307 $query = $this->wpdb->prepare(
308 "SELECT * FROM {$this->table_chats}
309 WHERE title IS NULL AND updated >= %s
310 ORDER BY updated DESC LIMIT 5",
311 $ten_days_ago
312 );
313 $discussions = $this->wpdb->get_results( $query );
314 if ( empty( $discussions ) ) {
315 $this->core->track_cron_end( 'mwai_discussions', 'success' );
316 return;
317 }
318
319 foreach ( $discussions as $discussion ) {
320 $this->generate_title_for_discussion( $discussion );
321 }
322
323 $this->core->track_cron_end( 'mwai_discussions', 'success' );
324 } catch ( Exception $e ) {
325 $this->core->track_cron_end( 'mwai_discussions', 'error', $e->getMessage() );
326 }
327 }
328
329 /**
330 * UI route for listing discussions.
331 * Here we add the "forced cron" logic for up to 5 discussions,
332 * but only if auto-titling is enabled.
333 */
334 public function rest_discussions_ui_list( $request ) {
335 try {
336 $params = $request->get_json_params();
337 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
338 // Get paging setting from options
339 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
340 if ( $paging_option === 'None' ) {
341 $default_limit = 999; // Show all discussions
342 }
343 else {
344 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
345 }
346 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
347 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
348 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
349
350 if ( !is_null( $customId ) ) {
351 $botId = $customId;
352 }
353 if ( is_null( $botId ) ) {
354 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
355 }
356
357 $userId = get_current_user_id();
358 if ( !$userId ) {
359 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
360 }
361
362 $filters = [
363 [ 'accessor' => 'user', 'value' => $userId ],
364 [ 'accessor' => 'botId', 'value' => $botId ],
365 ];
366
367 // Retrieve the chats
368 $chats = $this->chats_query( [], $offset, $limit, $filters );
369
370 // NEW CHECK: only do forced titling if it's enabled
371 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
372 // "Forced cron" logic: check up to 5 that have no title
373 $counter = 0;
374 foreach ( $chats['rows'] as &$chatRow ) {
375 if ( $counter >= 5 ) {
376 break;
377 }
378 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
379 $discussionObj = (object) $chatRow;
380 $this->generate_title_for_discussion( $discussionObj );
381 $counter++;
382 }
383 }
384 // If you want the newly-updated titles to show up *immediately*:
385 $chats = $this->chats_query( [], $offset, $limit, $filters );
386 }
387 // END NEW CHECK
388
389 // Apply filters to discussion metadata
390 foreach ( $chats['rows'] as &$chatRow ) {
391 // Decode messages JSON to get the count
392 $messages = json_decode( $chatRow['messages'], true );
393 $message_count = is_array( $messages ) ? count( $messages ) : 0;
394
395 // Add formatted metadata that can be filtered
396 $chatRow['metadata_display'] = [
397 'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
398 'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
399 'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
400 ];
401 }
402
403 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
404 }
405 catch ( Exception $e ) {
406 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
407 }
408 }
409
410 public function rest_discussions_delete_admin( $request ) {
411 try {
412 $params = $request->get_json_params();
413 $chatsIds = $params['chatIds'];
414 if ( is_array( $chatsIds ) ) {
415 if ( count( $chatsIds ) === 0 ) {
416 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
417 }
418 foreach ( $chatsIds as $chatId ) {
419 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
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 public function rest_discussions_delete( $request ) {
430 try {
431 $params = $request->get_json_params();
432 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
433
434 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
435 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
436 }
437
438 $userId = get_current_user_id();
439 if ( !$userId ) {
440 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
441 }
442
443 foreach ( $chatIds as $chatId ) {
444 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
445 }
446
447 return $this->create_rest_response( [ 'success' => true ], 200 );
448 }
449 catch ( Exception $e ) {
450 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
451 }
452 }
453
454 // Get latest discussion for the given parameter
455 public function get_discussion( $botId, $chatId ) {
456 $this->check_db();
457 $chat = $this->wpdb->get_row(
458 $this->wpdb->prepare(
459 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
460 $chatId,
461 $botId
462 ),
463 ARRAY_A
464 );
465 if ( $chat ) {
466 $chat['messages'] = json_decode( $chat['messages'] );
467 return $chat;
468 }
469 return null;
470 }
471
472 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
473 $this->check_db();
474 $offset = !empty( $offset ) ? intval( $offset ) : 0;
475 $limit = !empty( $limit ) ? intval( $limit ) : 5;
476 $filters = !empty( $filters ) ? $filters : [];
477 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
478
479 $where_clauses = [];
480 $where_values = [];
481
482 if ( is_array( $filters ) ) {
483 foreach ( $filters as $filter ) {
484 $value = $filter['value'];
485 if ( is_null( $value ) || $value === '' ) {
486 continue;
487 }
488 switch ( $filter['accessor'] ) {
489 case 'user':
490 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
491 if ( $isIP ) {
492 $where_clauses[] = 'ip = %s';
493 $where_values[] = $value;
494 }
495 else {
496 $where_clauses[] = 'userId = %d';
497 $where_values[] = intval( $value );
498 }
499 break;
500 case 'botId':
501 $where_clauses[] = 'botId = %s';
502 $where_values[] = $value;
503 break;
504 case 'preview':
505 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
506 $where_clauses[] = 'messages LIKE %s';
507 $where_values[] = $like;
508 break;
509 // Add other cases as needed
510 }
511 }
512 }
513
514 $where_sql = '';
515 if ( !empty( $where_clauses ) ) {
516 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
517 }
518 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
519
520 $limit_sql = '';
521 if ( $limit > 0 ) {
522 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
523 }
524
525 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
526 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
527
528 // Get the total count
529 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
530 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
531
532 return $chats;
533 }
534
535 public function chatbot_reply( $rawText, $query, $params, $extra ) {
536 global $mwai_core;
537 $userIp = $mwai_core->get_ip_address();
538 $userId = $mwai_core->get_user_id();
539 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
540 $chatId = $this->core->fix_chat_id( $query, $params );
541 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
542 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
543 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
544 $now = date( 'Y-m-d H:i:s' );
545
546 if ( !empty( $customId ) ) {
547 $botId = $customId;
548 }
549 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
550
551 // If there are files for "Vision", add them to the message
552 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
553 foreach ( $attachments as $attachedFile ) {
554 if ( $attachedFile->get_purpose() === 'vision' && $attachedFile->get_type() === 'url' ) {
555 $newMessage = "![Uploaded Image]({$attachedFile->get_url()})\n" . $newMessage;
556 }
557 }
558
559 $this->check_db();
560 $chat = $this->wpdb->get_row(
561 $this->wpdb->prepare(
562 "SELECT * FROM $this->table_chats WHERE chatId = %s",
563 $chatId
564 )
565 );
566 $messageExtra = [
567 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
568 ];
569 $chatExtra = [
570 'session' => $query->session,
571 'model' => $query->model,
572 ];
573 if ( !empty( $query->temperature ) ) {
574 $chatExtra['temperature'] = $query->temperature;
575 }
576 if ( !empty( $query->context ) ) {
577 $chatExtra['context'] = $query->context;
578 }
579 if ( !empty( $params['parentBotId'] ) ) {
580 $chatExtra['parentBotId'] = $params['parentBotId'];
581 }
582 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
583 $chatExtra['assistantId'] = $query->assistantId;
584 $chatExtra['threadId'] = $query->threadId;
585 $chatExtra['storeId'] = $query->storeId;
586 }
587
588 // Store response ID and date for Responses API
589 if ( !empty( $extra['responseId'] ) ) {
590 $chatExtra['previousResponseId'] = $extra['responseId'];
591 $chatExtra['previousResponseDate'] = $now;
592 }
593
594 if ( $chat ) {
595 $chat->messages = json_decode( $chat->messages );
596 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
597 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
598 $chat->messages = json_encode( $chat->messages );
599
600 // Update or merge extra data
601 $existingExtra = json_decode( $chat->extra, true ) ?: [];
602 $mergedExtra = array_merge( $existingExtra, $chatExtra );
603
604 $this->wpdb->update(
605 $this->table_chats,
606 [
607 'userId' => $userId,
608 'messages' => $chat->messages,
609 'extra' => json_encode( $mergedExtra ),
610 'updated' => $now
611 ],
612 [ 'id' => $chat->id ]
613 );
614 }
615 else {
616 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
617 $messages = [];
618 if ( !empty( $startSentence ) ) {
619 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
620 }
621 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
622 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
623 $chat = [
624 'userId' => $userId,
625 'ip' => $userIp,
626 'messages' => json_encode( $messages ),
627 'extra' => json_encode( $chatExtra ),
628 'botId' => $botId,
629 'chatId' => $chatId,
630 'threadId' => $threadId,
631 'storeId' => $storeId,
632 'created' => $now,
633 'updated' => $now
634 ];
635 $this->wpdb->insert( $this->table_chats, $chat );
636 }
637 return $rawText;
638 }
639
640 public function format_messages( $json, $format = 'html' ) {
641 $html = '';
642 if ( $format === 'html' ) {
643 try {
644 $conversation = json_decode( $json, true );
645 if ( json_last_error() !== JSON_ERROR_NONE ) {
646 return 'Invalid JSON format';
647 }
648 foreach ( $conversation as $message ) {
649 $role = ucfirst( $message['role'] );
650 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
651 }
652 }
653 catch ( Exception $e ) {
654 error_log( $e->getMessage() );
655 return 'Error while formatting the message';
656 }
657 }
658 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
659 return $html;
660 }
661
662 /**
663 * Commits a discussion into the database (create or update if the same chatId is found).
664 *
665 * @param Meow_MWAI_Discussion $discussionObject
666 * @return bool True if success, false if error
667 */
668 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
669 $this->check_db();
670
671 // 1. Check if a discussion with the same chatId already exists
672 $chat = $this->wpdb->get_row(
673 $this->wpdb->prepare(
674 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
675 $discussionObject->chatId
676 ),
677 ARRAY_A
678 );
679
680 // 2. Prepare data for DB
681 $userIp = $this->core->get_ip_address();
682 $userId = $this->core->get_user_id();
683 $now = date( 'Y-m-d H:i:s' );
684
685 $data = [
686 'userId' => $userId,
687 'ip' => $userIp,
688 'botId' => $discussionObject->botId,
689 'chatId' => $discussionObject->chatId,
690 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
691 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
692 'updated' => $now,
693 ];
694
695 // 3. Update if found, otherwise insert a new row
696 if ( $chat ) {
697 $updateRes = $this->wpdb->update(
698 $this->table_chats,
699 $data,
700 [ 'id' => $chat['id'] ]
701 );
702 if ( $updateRes === false ) {
703 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
704 return false;
705 }
706 }
707 else {
708 // For insertion, also set "created"
709 $data['created'] = $now;
710 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
711 if ( $insertRes === false ) {
712 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
713 return false;
714 }
715 }
716
717 return true;
718 }
719
720 public function check_db() {
721 if ( $this->db_check ) {
722 return true;
723 }
724 $this->db_check = !(
725 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
726 != strtolower( $this->table_chats )
727 );
728 if ( !$this->db_check ) {
729 $this->create_db();
730 $this->db_check = !(
731 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
732 != strtolower( $this->table_chats )
733 );
734 }
735
736 // LATER: REMOVE THIS AFTER MARCH 2025
737 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_chats LIKE 'title'" );
738 // if ( ! $this->db_check ) {
739 // $this->wpdb->query( "ALTER TABLE $this->table_chats ADD COLUMN title VARCHAR(64) NULL" );
740 // $this->db_check = true;
741 // }
742
743 // LATER: REMOVE THIS AFTER SEPTEMBER 2025
744 // Migrate guest users from userId = 0 to userId = NULL
745 $guest_count = $this->wpdb->get_var( "SELECT COUNT(*) FROM $this->table_chats WHERE userId = 0" );
746 if ( $guest_count > 0 ) {
747 $this->wpdb->query( "UPDATE $this->table_chats SET userId = NULL WHERE userId = 0" );
748 }
749
750 return $this->db_check;
751 }
752
753 public function create_db() {
754 $charset_collate = $this->wpdb->get_charset_collate();
755 $sqlLogs = "CREATE TABLE $this->table_chats (
756 id BIGINT(20) NOT NULL AUTO_INCREMENT,
757 userId BIGINT(20) NULL,
758 ip VARCHAR(64) NULL,
759 title VARCHAR(64) NULL,
760 messages TEXT NOT NULL NULL,
761 extra LONGTEXT NOT NULL NULL,
762 botId VARCHAR(64) NULL,
763 chatId VARCHAR(64) NOT NULL,
764 threadId VARCHAR(64) NULL,
765 storeId VARCHAR(64) NULL,
766 created DATETIME NOT NULL,
767 updated DATETIME NOT NULL,
768 PRIMARY KEY (id),
769 INDEX chatId (chatId)
770 ) $charset_collate;";
771 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
772 dbDelta( $sqlLogs );
773 }
774
775 /**
776 * Handle cleanup task for discussions
777 */
778 public function handle_cleanup_task( $result, $job ) {
779 $start = microtime( true );
780 $retention_days = 90; // 3 months retention period
781 $cutoff = date( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
782
783 // Check if discussions table exists
784 $table_exists = $this->wpdb->get_var( "SHOW TABLES LIKE '{$this->table_chats}'" );
785 if ( !$table_exists ) {
786 return [
787 'ok' => true,
788 'done' => true,
789 'message' => 'Discussions table does not exist yet',
790 ];
791 }
792
793 // Get current progress
794 $deleted_total = isset( $job['meta']['deleted_total'] ) ? (int) $job['meta']['deleted_total'] : 0;
795 $last_id = isset( $job['meta']['last_id'] ) ? (int) $job['meta']['last_id'] : 0;
796
797 // Delete in batches
798 $batch_size = 100;
799 $deleted_batch = 0;
800
801 $old_discussions = $this->wpdb->get_results( $this->wpdb->prepare(
802 "SELECT id FROM {$this->table_chats}
803 WHERE updated < %s AND id > %d
804 ORDER BY id ASC
805 LIMIT %d",
806 $cutoff, $last_id, $batch_size
807 ) );
808
809 if ( !empty( $old_discussions ) ) {
810 $ids = wp_list_pluck( $old_discussions, 'id' );
811 $ids_string = implode( ',', array_map( 'intval', $ids ) );
812
813 $deleted_batch = $this->wpdb->query(
814 "DELETE FROM {$this->table_chats} WHERE id IN ($ids_string)"
815 );
816
817 $deleted_total += $deleted_batch;
818 $last_id = end( $ids );
819 }
820
821 // Check if we have more to process or time is running out
822 $has_more = count( $old_discussions ) === $batch_size;
823 $time_elapsed = microtime( true ) - $start;
824
825 if ( $has_more && $time_elapsed < 8 ) {
826 // Continue processing
827 return [
828 'ok' => true,
829 'done' => false,
830 'message' => sprintf( 'Deleted %d discussions (total: %d)', $deleted_batch, $deleted_total ),
831 'meta' => [
832 'deleted_total' => $deleted_total,
833 'last_id' => $last_id,
834 ],
835 'step' => $job['step'] + 1,
836 'step_name' => 'batch_' . ( $job['step'] + 1 ),
837 ];
838 }
839
840 // Completed
841 return [
842 'ok' => true,
843 'done' => true,
844 'message' => sprintf( 'Cleanup complete. Deleted %d discussions older than %d days', $deleted_total, $retention_days ),
845 'meta' => [
846 'deleted_total' => 0,
847 'last_id' => 0,
848 ],
849 ];
850 }
851 }
852