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