PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.1
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 6 months ago discussions.php 6 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 7 months ago wand.php 7 months ago
discussions.php
843 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, $query, $params, $extra ) {
539 global $mwai_core;
540 $userIp = $mwai_core->get_ip_address();
541 $userId = $mwai_core->get_user_id();
542 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
543 $chatId = $this->core->fix_chat_id( $query, $params );
544 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
545 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
546 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
547 $now = date( 'Y-m-d H:i:s' );
548
549 if ( !empty( $customId ) ) {
550 $botId = $customId;
551 }
552 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
553
554 // If there are images, add them to the message for display purposes
555 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
556 foreach ( $attachments as $attachedFile ) {
557 if ( $attachedFile->is_image() && $attachedFile->get_type() === 'url' ) {
558 $newMessage = "![Uploaded Image]({$attachedFile->get_url()})\n" . $newMessage;
559 }
560 }
561
562 $this->check_db();
563 $chat = $this->wpdb->get_row(
564 $this->wpdb->prepare(
565 "SELECT * FROM $this->table_chats WHERE chatId = %s",
566 $chatId
567 )
568 );
569 $messageExtra = [
570 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
571 ];
572 $chatExtra = [
573 'session' => $query->session,
574 'model' => $query->model,
575 ];
576 if ( !empty( $query->temperature ) ) {
577 $chatExtra['temperature'] = $query->temperature;
578 }
579 if ( !empty( $query->context ) ) {
580 $chatExtra['context'] = $query->context;
581 }
582 if ( !empty( $params['parentBotId'] ) ) {
583 $chatExtra['parentBotId'] = $params['parentBotId'];
584 }
585 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
586 $chatExtra['assistantId'] = $query->assistantId;
587 $chatExtra['threadId'] = $query->threadId;
588 $chatExtra['storeId'] = $query->storeId;
589 }
590
591 // Store response ID and date for Responses API
592 if ( !empty( $extra['responseId'] ) ) {
593 $chatExtra['previousResponseId'] = $extra['responseId'];
594 $chatExtra['previousResponseDate'] = $now;
595 }
596
597 if ( $chat ) {
598 $chat->messages = json_decode( $chat->messages );
599 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
600 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
601 $chat->messages = json_encode( $chat->messages );
602
603 // Update or merge extra data
604 $existingExtra = json_decode( $chat->extra, true ) ?: [];
605 $mergedExtra = array_merge( $existingExtra, $chatExtra );
606
607 $this->wpdb->update(
608 $this->table_chats,
609 [
610 'userId' => $userId,
611 'messages' => $chat->messages,
612 'extra' => json_encode( $mergedExtra ),
613 'updated' => $now
614 ],
615 [ 'id' => $chat->id ]
616 );
617 }
618 else {
619 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
620 $messages = [];
621 if ( !empty( $startSentence ) ) {
622 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
623 }
624 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
625 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
626 $chat = [
627 'userId' => $userId,
628 'ip' => $userIp,
629 'messages' => json_encode( $messages ),
630 'extra' => json_encode( $chatExtra ),
631 'botId' => $botId,
632 'chatId' => $chatId,
633 'threadId' => $threadId,
634 'storeId' => $storeId,
635 'created' => $now,
636 'updated' => $now
637 ];
638 $this->wpdb->insert( $this->table_chats, $chat );
639 }
640 return $rawText;
641 }
642
643 public function format_messages( $json, $format = 'html' ) {
644 $html = '';
645 if ( $format === 'html' ) {
646 try {
647 $conversation = json_decode( $json, true );
648 if ( json_last_error() !== JSON_ERROR_NONE ) {
649 return 'Invalid JSON format';
650 }
651 foreach ( $conversation as $message ) {
652 $role = ucfirst( $message['role'] );
653 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
654 }
655 }
656 catch ( Exception $e ) {
657 error_log( $e->getMessage() );
658 return 'Error while formatting the message';
659 }
660 }
661 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
662 return $html;
663 }
664
665 /**
666 * Commits a discussion into the database (create or update if the same chatId is found).
667 *
668 * @param Meow_MWAI_Discussion $discussionObject
669 * @return bool True if success, false if error
670 */
671 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
672 $this->check_db();
673
674 // 1. Check if a discussion with the same chatId already exists
675 $chat = $this->wpdb->get_row(
676 $this->wpdb->prepare(
677 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
678 $discussionObject->chatId
679 ),
680 ARRAY_A
681 );
682
683 // 2. Prepare data for DB
684 $userIp = $this->core->get_ip_address();
685 $userId = $this->core->get_user_id();
686 $now = date( 'Y-m-d H:i:s' );
687
688 $data = [
689 'userId' => $userId,
690 'ip' => $userIp,
691 'botId' => $discussionObject->botId,
692 'chatId' => $discussionObject->chatId,
693 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
694 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
695 'updated' => $now,
696 ];
697
698 // 3. Update if found, otherwise insert a new row
699 if ( $chat ) {
700 $updateRes = $this->wpdb->update(
701 $this->table_chats,
702 $data,
703 [ 'id' => $chat['id'] ]
704 );
705 if ( $updateRes === false ) {
706 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
707 return false;
708 }
709 }
710 else {
711 // For insertion, also set "created"
712 $data['created'] = $now;
713 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
714 if ( $insertRes === false ) {
715 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
716 return false;
717 }
718 }
719
720 return true;
721 }
722
723 public function check_db() {
724 if ( $this->db_check ) {
725 return true;
726 }
727 $this->db_check = !(
728 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
729 != strtolower( $this->table_chats )
730 );
731 if ( !$this->db_check ) {
732 $this->create_db();
733 $this->db_check = !(
734 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
735 != strtolower( $this->table_chats )
736 );
737 }
738
739 return $this->db_check;
740 }
741
742 public function create_db() {
743 $charset_collate = $this->wpdb->get_charset_collate();
744 $sqlLogs = "CREATE TABLE $this->table_chats (
745 id BIGINT(20) NOT NULL AUTO_INCREMENT,
746 userId BIGINT(20) NULL,
747 ip VARCHAR(64) NULL,
748 title VARCHAR(64) NULL,
749 messages TEXT NOT NULL NULL,
750 extra LONGTEXT NOT NULL NULL,
751 botId VARCHAR(64) NULL,
752 chatId VARCHAR(64) NOT NULL,
753 threadId VARCHAR(64) NULL,
754 storeId VARCHAR(64) NULL,
755 created DATETIME NOT NULL,
756 updated DATETIME NOT NULL,
757 PRIMARY KEY (id),
758 INDEX chatId (chatId)
759 ) $charset_collate;";
760 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
761 dbDelta( $sqlLogs );
762 }
763
764 /**
765 * Handle cleanup task for discussions
766 */
767 public function handle_cleanup_task( $result, $job ) {
768 $start = microtime( true );
769 $retention_days = 90; // 3 months retention period
770 $cutoff = date( 'Y-m-d H:i:s', strtotime( "-{$retention_days} days" ) );
771
772 // Check if discussions table exists
773 $table_exists = $this->wpdb->get_var( "SHOW TABLES LIKE '{$this->table_chats}'" );
774 if ( !$table_exists ) {
775 return [
776 'ok' => true,
777 'done' => true,
778 'message' => 'Discussions table does not exist yet',
779 ];
780 }
781
782 // Get current progress
783 $deleted_total = isset( $job['meta']['deleted_total'] ) ? (int) $job['meta']['deleted_total'] : 0;
784 $last_id = isset( $job['meta']['last_id'] ) ? (int) $job['meta']['last_id'] : 0;
785
786 // Delete in batches
787 $batch_size = 100;
788 $deleted_batch = 0;
789
790 $old_discussions = $this->wpdb->get_results( $this->wpdb->prepare(
791 "SELECT id FROM {$this->table_chats}
792 WHERE updated < %s AND id > %d
793 ORDER BY id ASC
794 LIMIT %d",
795 $cutoff,
796 $last_id,
797 $batch_size
798 ) );
799
800 if ( !empty( $old_discussions ) ) {
801 $ids = wp_list_pluck( $old_discussions, 'id' );
802 $ids_string = implode( ',', array_map( 'intval', $ids ) );
803
804 $deleted_batch = $this->wpdb->query(
805 "DELETE FROM {$this->table_chats} WHERE id IN ($ids_string)"
806 );
807
808 $deleted_total += $deleted_batch;
809 $last_id = end( $ids );
810 }
811
812 // Check if we have more to process or time is running out
813 $has_more = count( $old_discussions ) === $batch_size;
814 $time_elapsed = microtime( true ) - $start;
815
816 if ( $has_more && $time_elapsed < 8 ) {
817 // Continue processing
818 return [
819 'ok' => true,
820 'done' => false,
821 'message' => sprintf( 'Deleted %d discussions (total: %d)', $deleted_batch, $deleted_total ),
822 'meta' => [
823 'deleted_total' => $deleted_total,
824 'last_id' => $last_id,
825 ],
826 'step' => $job['step'] + 1,
827 'step_name' => 'batch_' . ( $job['step'] + 1 ),
828 ];
829 }
830
831 // Completed
832 return [
833 'ok' => true,
834 'done' => true,
835 'message' => sprintf( 'Cleanup complete. Deleted %d discussions older than %d days', $deleted_total, $retention_days ),
836 'meta' => [
837 'deleted_total' => 0,
838 'last_id' => 0,
839 ],
840 ];
841 }
842 }
843