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