PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.3
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 5 months ago discussions.php 5 months ago files.php 6 months ago forms-manager.php 10 months ago gdpr.php 11 months ago search.php 1 year ago security.php 1 year ago tasks-examples.php 6 months ago tasks.php 5 months ago wand.php 5 months ago
discussions.php
844 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 }
206 catch ( Exception $e ) {
207 // Handle content filter or other API errors
208 $error_message = $e->getMessage();
209 if ( strpos( $error_message, 'content_filter' ) !== false ||
210 strpos( $error_message, 'ResponsibleAIPolicyViolation' ) !== false ) {
211 error_log( "AI Engine: Content filter blocked title generation for discussion ID {$discussion->id}. Using fallback title." );
212 $title = __( 'Discussion', 'ai-engine' ) . ' ' . date( 'Y-m-d H:i' );
213 }
214 else {
215 error_log( "AI Engine: Failed to generate title for discussion ID {$discussion->id}: " . $error_message );
216 $title = __( 'Untitled', 'ai-engine' );
217 }
218 }
219
220 // Update the discussion with the title
221 $updated = $this->wpdb->update(
222 $this->table_chats,
223 [ 'title' => $title ],
224 [ 'id' => $discussion->id ]
225 );
226 if ( $updated === false ) {
227 error_log( "Failed to update the title for discussion ID {$discussion->id}" );
228 }
229 }
230
231 /**
232 * Admin route for listing discussions. No forced logic here.
233 */
234 public function rest_discussions_list( $request ) {
235 try {
236 $params = $request->get_json_params();
237 $offset = $params['offset'];
238 $limit = $params['limit'];
239 $filters = $params['filters'];
240 $sort = $params['sort'];
241
242 // Retrieve the chats
243 $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );
244
245 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
246 }
247 catch ( Exception $e ) {
248 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
249 }
250 }
251
252 public function rest_discussions_ui_edit( $request ) {
253 try {
254 $params = $request->get_json_params();
255 $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
256 $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;
257
258 if ( is_null( $chatId ) || is_null( $title ) ) {
259 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
260 }
261
262 $userId = get_current_user_id();
263 if ( !$userId ) {
264 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
265 }
266
267 // Update the discussion title for the current user
268 $updated = $this->wpdb->update(
269 $this->table_chats,
270 [ 'title' => $title ],
271 [ 'chatId' => $chatId, 'userId' => $userId ]
272 );
273 if ( $updated === false ) {
274 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
275 }
276
277 return $this->create_rest_response( [ 'success' => true ], 200 );
278 }
279 catch ( Exception $e ) {
280 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
281 }
282 }
283
284 public function cron_discussions() {
285 // Track cron execution start
286 $this->core->track_cron_start( 'mwai_discussions' );
287
288 try {
289 $this->check_db();
290
291 // NEW CHECK: Only run if auto-titling is enabled
292 if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
293 $this->core->track_cron_end( 'mwai_discussions', 'success' );
294 return;
295 }
296 // END NEW CHECK
297
298 // Set the current user to the first admin to avoid guest limits
299 $admin_users = get_users( [ 'role' => 'administrator', 'number' => 1 ] );
300 if ( !empty( $admin_users ) ) {
301 $admin_user = $admin_users[0];
302 wp_set_current_user( $admin_user->ID );
303 }
304
305 $now = date( 'Y-m-d H:i:s' );
306 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
307
308 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
309 $query = $this->wpdb->prepare(
310 "SELECT * FROM {$this->table_chats}
311 WHERE title IS NULL AND updated >= %s
312 ORDER BY updated DESC LIMIT 5",
313 $ten_days_ago
314 );
315 $discussions = $this->wpdb->get_results( $query );
316 if ( empty( $discussions ) ) {
317 $this->core->track_cron_end( 'mwai_discussions', 'success' );
318 return;
319 }
320
321 foreach ( $discussions as $discussion ) {
322 $this->generate_title_for_discussion( $discussion );
323 }
324
325 $this->core->track_cron_end( 'mwai_discussions', 'success' );
326 }
327 catch ( Exception $e ) {
328 $this->core->track_cron_end( 'mwai_discussions', 'error', $e->getMessage() );
329 }
330 }
331
332 /**
333 * UI route for listing discussions.
334 * Here we add the "forced cron" logic for up to 5 discussions,
335 * but only if auto-titling is enabled.
336 */
337 public function rest_discussions_ui_list( $request ) {
338 try {
339 $params = $request->get_json_params();
340 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
341 // Get paging setting from options
342 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
343 if ( $paging_option === 'None' ) {
344 $default_limit = 999; // Show all discussions
345 }
346 else {
347 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
348 }
349 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
350 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
351 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
352
353 if ( !is_null( $customId ) ) {
354 $botId = $customId;
355 }
356 if ( is_null( $botId ) ) {
357 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
358 }
359
360 $userId = get_current_user_id();
361 if ( !$userId ) {
362 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
363 }
364
365 $filters = [
366 [ 'accessor' => 'user', 'value' => $userId ],
367 [ 'accessor' => 'botId', 'value' => $botId ],
368 ];
369
370 // Retrieve the chats
371 $chats = $this->chats_query( [], $offset, $limit, $filters );
372
373 // NEW CHECK: only do forced titling if it's enabled
374 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
375 // "Forced cron" logic: check up to 5 that have no title
376 $counter = 0;
377 foreach ( $chats['rows'] as &$chatRow ) {
378 if ( $counter >= 5 ) {
379 break;
380 }
381 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
382 $discussionObj = (object) $chatRow;
383 $this->generate_title_for_discussion( $discussionObj );
384 $counter++;
385 }
386 }
387 // If you want the newly-updated titles to show up *immediately*:
388 $chats = $this->chats_query( [], $offset, $limit, $filters );
389 }
390 // END NEW CHECK
391
392 // Apply filters to discussion metadata
393 foreach ( $chats['rows'] as &$chatRow ) {
394 // Decode messages JSON to get the count
395 $messages = json_decode( $chatRow['messages'], true );
396 $message_count = is_array( $messages ) ? count( $messages ) : 0;
397
398 // Add formatted metadata that can be filtered
399 $chatRow['metadata_display'] = [
400 'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
401 'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
402 'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
403 ];
404 }
405
406 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
407 }
408 catch ( Exception $e ) {
409 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
410 }
411 }
412
413 public function rest_discussions_delete_admin( $request ) {
414 try {
415 $params = $request->get_json_params();
416 $chatsIds = $params['chatIds'];
417 if ( is_array( $chatsIds ) ) {
418 if ( count( $chatsIds ) === 0 ) {
419 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
420 }
421 foreach ( $chatsIds as $chatId ) {
422 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
423 }
424 }
425 return $this->create_rest_response( [ 'success' => true ], 200 );
426 }
427 catch ( Exception $e ) {
428 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
429 }
430 }
431
432 public function rest_discussions_delete( $request ) {
433 try {
434 $params = $request->get_json_params();
435 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
436
437 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
438 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
439 }
440
441 $userId = get_current_user_id();
442 if ( !$userId ) {
443 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
444 }
445
446 foreach ( $chatIds as $chatId ) {
447 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
448 }
449
450 return $this->create_rest_response( [ 'success' => true ], 200 );
451 }
452 catch ( Exception $e ) {
453 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
454 }
455 }
456
457 // Get latest discussion for the given parameter
458 public function get_discussion( $botId, $chatId ) {
459 $this->check_db();
460 $chat = $this->wpdb->get_row(
461 $this->wpdb->prepare(
462 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
463 $chatId,
464 $botId
465 ),
466 ARRAY_A
467 );
468 if ( $chat ) {
469 $chat['messages'] = json_decode( $chat['messages'] );
470 return $chat;
471 }
472 return null;
473 }
474
475 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
476 $this->check_db();
477 $offset = !empty( $offset ) ? intval( $offset ) : 0;
478 $limit = !empty( $limit ) ? intval( $limit ) : 5;
479 $filters = !empty( $filters ) ? $filters : [];
480 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
481
482 $where_clauses = [];
483 $where_values = [];
484
485 if ( is_array( $filters ) ) {
486 foreach ( $filters as $filter ) {
487 $value = $filter['value'];
488 if ( is_null( $value ) || $value === '' ) {
489 continue;
490 }
491 switch ( $filter['accessor'] ) {
492 case 'user':
493 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
494 if ( $isIP ) {
495 $where_clauses[] = 'ip = %s';
496 $where_values[] = $value;
497 }
498 else {
499 $where_clauses[] = 'userId = %d';
500 $where_values[] = intval( $value );
501 }
502 break;
503 case 'botId':
504 $where_clauses[] = 'botId = %s';
505 $where_values[] = $value;
506 break;
507 case 'preview':
508 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
509 $where_clauses[] = 'messages LIKE %s';
510 $where_values[] = $like;
511 break;
512 // Add other cases as needed
513 }
514 }
515 }
516
517 $where_sql = '';
518 if ( !empty( $where_clauses ) ) {
519 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
520 }
521 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
522
523 $limit_sql = '';
524 if ( $limit > 0 ) {
525 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
526 }
527
528 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
529 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
530
531 // Get the total count
532 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
533 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
534
535 return $chats;
536 }
537
538 public function chatbot_reply( $rawText, $reply, $params, $extra ) {
539 global $mwai_core;
540 $query = $reply->query;
541 $userIp = $mwai_core->get_ip_address();
542 $userId = $mwai_core->get_user_id();
543 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
544 $chatId = $this->core->fix_chat_id( $query, $params );
545 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
546 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
547 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
548 $now = date( 'Y-m-d H:i:s' );
549
550 if ( !empty( $customId ) ) {
551 $botId = $customId;
552 }
553 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
554
555 // If there are images, add them to the message for display purposes
556 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
557 foreach ( $attachments as $attachedFile ) {
558 if ( $attachedFile->is_image() && $attachedFile->get_type() === 'url' ) {
559 $newMessage = "![Uploaded Image]({$attachedFile->get_url()})\n" . $newMessage;
560 }
561 }
562
563 $this->check_db();
564 $chat = $this->wpdb->get_row(
565 $this->wpdb->prepare(
566 "SELECT * FROM $this->table_chats WHERE chatId = %s",
567 $chatId
568 )
569 );
570 $messageExtra = [
571 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
572 ];
573 $chatExtra = [
574 'session' => $query->session,
575 'model' => $query->model,
576 ];
577 if ( !empty( $query->temperature ) ) {
578 $chatExtra['temperature'] = $query->temperature;
579 }
580 if ( !empty( $query->context ) ) {
581 $chatExtra['context'] = $query->context;
582 }
583 if ( !empty( $params['parentBotId'] ) ) {
584 $chatExtra['parentBotId'] = $params['parentBotId'];
585 }
586 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
587 $chatExtra['assistantId'] = $query->assistantId;
588 $chatExtra['threadId'] = $query->threadId;
589 $chatExtra['storeId'] = $query->storeId;
590 }
591
592 // Store response ID and date for Responses API
593 if ( !empty( $extra['responseId'] ) ) {
594 $chatExtra['previousResponseId'] = $extra['responseId'];
595 $chatExtra['previousResponseDate'] = $now;
596 }
597
598 if ( $chat ) {
599 $chat->messages = json_decode( $chat->messages );
600 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
601 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
602 $chat->messages = json_encode( $chat->messages );
603
604 // Update or merge extra data
605 $existingExtra = json_decode( $chat->extra, true ) ?: [];
606 $mergedExtra = array_merge( $existingExtra, $chatExtra );
607
608 $this->wpdb->update(
609 $this->table_chats,
610 [
611 'userId' => $userId,
612 'messages' => $chat->messages,
613 'extra' => json_encode( $mergedExtra ),
614 'updated' => $now
615 ],
616 [ 'id' => $chat->id ]
617 );
618 }
619 else {
620 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
621 $messages = [];
622 if ( !empty( $startSentence ) ) {
623 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
624 }
625 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
626 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
627 $chat = [
628 'userId' => $userId,
629 'ip' => $userIp,
630 'messages' => json_encode( $messages ),
631 'extra' => json_encode( $chatExtra ),
632 'botId' => $botId,
633 'chatId' => $chatId,
634 'threadId' => $threadId,
635 'storeId' => $storeId,
636 'created' => $now,
637 'updated' => $now
638 ];
639 $this->wpdb->insert( $this->table_chats, $chat );
640 }
641 return $rawText;
642 }
643
644 public function format_messages( $json, $format = 'html' ) {
645 $html = '';
646 if ( $format === 'html' ) {
647 try {
648 $conversation = json_decode( $json, true );
649 if ( json_last_error() !== JSON_ERROR_NONE ) {
650 return 'Invalid JSON format';
651 }
652 foreach ( $conversation as $message ) {
653 $role = ucfirst( $message['role'] );
654 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
655 }
656 }
657 catch ( Exception $e ) {
658 error_log( $e->getMessage() );
659 return 'Error while formatting the message';
660 }
661 }
662 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
663 return $html;
664 }
665
666 /**
667 * Commits a discussion into the database (create or update if the same chatId is found).
668 *
669 * @param Meow_MWAI_Discussion $discussionObject
670 * @return bool True if success, false if error
671 */
672 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
673 $this->check_db();
674
675 // 1. Check if a discussion with the same chatId already exists
676 $chat = $this->wpdb->get_row(
677 $this->wpdb->prepare(
678 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
679 $discussionObject->chatId
680 ),
681 ARRAY_A
682 );
683
684 // 2. Prepare data for DB
685 $userIp = $this->core->get_ip_address();
686 $userId = $this->core->get_user_id();
687 $now = date( 'Y-m-d H:i:s' );
688
689 $data = [
690 'userId' => $userId,
691 'ip' => $userIp,
692 'botId' => $discussionObject->botId,
693 'chatId' => $discussionObject->chatId,
694 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
695 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
696 'updated' => $now,
697 ];
698
699 // 3. Update if found, otherwise insert a new row
700 if ( $chat ) {
701 $updateRes = $this->wpdb->update(
702 $this->table_chats,
703 $data,
704 [ 'id' => $chat['id'] ]
705 );
706 if ( $updateRes === false ) {
707 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
708 return false;
709 }
710 }
711 else {
712 // For insertion, also set "created"
713 $data['created'] = $now;
714 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
715 if ( $insertRes === false ) {
716 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
717 return false;
718 }
719 }
720
721 return true;
722 }
723
724 public function check_db() {
725 if ( $this->db_check ) {
726 return true;
727 }
728 $this->db_check = !(
729 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
730 != strtolower( $this->table_chats )
731 );
732 if ( !$this->db_check ) {
733 $this->create_db();
734 $this->db_check = !(
735 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
736 != strtolower( $this->table_chats )
737 );
738 }
739
740 return $this->db_check;
741 }
742
743 public function create_db() {
744 $charset_collate = $this->wpdb->get_charset_collate();
745 $sqlLogs = "CREATE TABLE $this->table_chats (
746 id BIGINT(20) NOT NULL AUTO_INCREMENT,
747 userId BIGINT(20) NULL,
748 ip VARCHAR(64) NULL,
749 title VARCHAR(64) NULL,
750 messages TEXT NOT NULL NULL,
751 extra LONGTEXT NOT NULL NULL,
752 botId VARCHAR(64) NULL,
753 chatId VARCHAR(64) NOT NULL,
754 threadId VARCHAR(64) NULL,
755 storeId VARCHAR(64) NULL,
756 created DATETIME NOT NULL,
757 updated DATETIME NOT NULL,
758 PRIMARY KEY (id),
759 INDEX chatId (chatId)
760 ) $charset_collate;";
761 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
762 dbDelta( $sqlLogs );
763 }
764
765 /**
766 * Handle cleanup task for discussions
767 */
768 public function handle_cleanup_task( $result, $job ) {
769 $start = microtime( true );
770 $retention_days = 90; // 3 months retention period
771 $cutoff = date( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
772
773 // Check if discussions table exists
774 $table_exists = $this->wpdb->get_var( "SHOW TABLES LIKE '{$this->table_chats}'" );
775 if ( !$table_exists ) {
776 return [
777 'ok' => true,
778 'done' => true,
779 'message' => 'Discussions table does not exist yet',
780 ];
781 }
782
783 // Get current progress
784 $deleted_total = isset( $job['meta']['deleted_total'] ) ? (int) $job['meta']['deleted_total'] : 0;
785 $last_id = isset( $job['meta']['last_id'] ) ? (int) $job['meta']['last_id'] : 0;
786
787 // Delete in batches
788 $batch_size = 100;
789 $deleted_batch = 0;
790
791 $old_discussions = $this->wpdb->get_results( $this->wpdb->prepare(
792 "SELECT id FROM {$this->table_chats}
793 WHERE updated < %s AND id > %d
794 ORDER BY id ASC
795 LIMIT %d",
796 $cutoff,
797 $last_id,
798 $batch_size
799 ) );
800
801 if ( !empty( $old_discussions ) ) {
802 $ids = wp_list_pluck( $old_discussions, 'id' );
803 $ids_string = implode( ',', array_map( 'intval', $ids ) );
804
805 $deleted_batch = $this->wpdb->query(
806 "DELETE FROM {$this->table_chats} WHERE id IN ($ids_string)"
807 );
808
809 $deleted_total += $deleted_batch;
810 $last_id = end( $ids );
811 }
812
813 // Check if we have more to process or time is running out
814 $has_more = count( $old_discussions ) === $batch_size;
815 $time_elapsed = microtime( true ) - $start;
816
817 if ( $has_more && $time_elapsed < 8 ) {
818 // Continue processing
819 return [
820 'ok' => true,
821 'done' => false,
822 'message' => sprintf( 'Deleted %d discussions (total: %d)', $deleted_batch, $deleted_total ),
823 'meta' => [
824 'deleted_total' => $deleted_total,
825 'last_id' => $last_id,
826 ],
827 'step' => $job['step'] + 1,
828 'step_name' => 'batch_' . ( $job['step'] + 1 ),
829 ];
830 }
831
832 // Completed
833 return [
834 'ok' => true,
835 'done' => true,
836 'message' => sprintf( 'Cleanup complete. Deleted %d discussions older than %d days', $deleted_total, $retention_days ),
837 'meta' => [
838 'deleted_total' => 0,
839 'last_id' => 0,
840 ],
841 ];
842 }
843 }
844