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