PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.3
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / modules / discussions.php
ai-engine / classes / modules Last commit date
advisor.php 1 year ago chatbot.php 11 months ago discussions.php 11 months ago files.php 1 year ago gdpr.php 1 year ago search.php 1 year ago security.php 1 year ago tasks.php 1 year ago wand.php 1 year ago
discussions.php
745 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 $fastEnv = $this->core->get_option( 'ai_fast_default_env' );
190 $fastModel = $this->core->get_option( 'ai_fast_default_model' );
191
192 if ( !empty( $fastEnv ) ) {
193 $params['envId'] = $fastEnv;
194 }
195 if ( !empty( $fastModel ) ) {
196 $params['model'] = $fastModel;
197 }
198
199 $answer = $mwai->simpleTextQuery( $prompt, $params );
200
201 // Clean up the answer
202 $title = trim( $answer );
203 $title = rtrim( $title, '.!?:;,—–-–' ); // Remove trailing punctuation
204 $title = substr( $title, 0, 64 ); // Ensure less than 64 characters
205 if ( empty( $title ) ) {
206 $title = 'Untitled';
207 }
208
209 // Update the discussion with the title
210 $updated = $this->wpdb->update(
211 $this->table_chats,
212 [ 'title' => $title ],
213 [ 'id' => $discussion->id ]
214 );
215 if ( $updated === false ) {
216 error_log( "Failed to update the title for discussion ID {$discussion->id}" );
217 }
218 }
219
220 /**
221 * Admin route for listing discussions. No forced logic here.
222 */
223 public function rest_discussions_list( $request ) {
224 try {
225 $params = $request->get_json_params();
226 $offset = $params['offset'];
227 $limit = $params['limit'];
228 $filters = $params['filters'];
229 $sort = $params['sort'];
230
231 // Retrieve the chats
232 $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );
233
234 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
235 }
236 catch ( Exception $e ) {
237 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
238 }
239 }
240
241 public function rest_discussions_ui_edit( $request ) {
242 try {
243 $params = $request->get_json_params();
244 $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
245 $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;
246
247 if ( is_null( $chatId ) || is_null( $title ) ) {
248 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
249 }
250
251 $userId = get_current_user_id();
252 if ( !$userId ) {
253 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
254 }
255
256 // Update the discussion title for the current user
257 $updated = $this->wpdb->update(
258 $this->table_chats,
259 [ 'title' => $title ],
260 [ 'chatId' => $chatId, 'userId' => $userId ]
261 );
262 if ( $updated === false ) {
263 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
264 }
265
266 return $this->create_rest_response( [ 'success' => true ], 200 );
267 }
268 catch ( Exception $e ) {
269 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
270 }
271 }
272
273 public function cron_discussions() {
274 $this->check_db();
275
276 // NEW CHECK: Only run if auto-titling is enabled
277 if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
278 return;
279 }
280 // END NEW CHECK
281
282 $now = date( 'Y-m-d H:i:s' );
283 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
284
285 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
286 $query = $this->wpdb->prepare(
287 "SELECT * FROM {$this->table_chats}
288 WHERE title IS NULL AND updated >= %s
289 ORDER BY updated DESC LIMIT 5",
290 $ten_days_ago
291 );
292 $discussions = $this->wpdb->get_results( $query );
293 if ( empty( $discussions ) ) {
294 return;
295 }
296
297 foreach ( $discussions as $discussion ) {
298 $this->generate_title_for_discussion( $discussion );
299 }
300 }
301
302 /**
303 * UI route for listing discussions.
304 * Here we add the "forced cron" logic for up to 5 discussions,
305 * but only if auto-titling is enabled.
306 */
307 public function rest_discussions_ui_list( $request ) {
308 try {
309 $params = $request->get_json_params();
310 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
311 // Get paging setting from options
312 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
313 if ( $paging_option === 'None' ) {
314 $default_limit = 999; // Show all discussions
315 }
316 else {
317 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
318 }
319 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
320 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
321 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
322
323 if ( !is_null( $customId ) ) {
324 $botId = $customId;
325 }
326 if ( is_null( $botId ) ) {
327 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
328 }
329
330 $userId = get_current_user_id();
331 if ( !$userId ) {
332 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
333 }
334
335 $filters = [
336 [ 'accessor' => 'user', 'value' => $userId ],
337 [ 'accessor' => 'botId', 'value' => $botId ],
338 ];
339
340 // Retrieve the chats
341 $chats = $this->chats_query( [], $offset, $limit, $filters );
342
343 // NEW CHECK: only do forced titling if it's enabled
344 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
345 // "Forced cron" logic: check up to 5 that have no title
346 $counter = 0;
347 foreach ( $chats['rows'] as &$chatRow ) {
348 if ( $counter >= 5 ) {
349 break;
350 }
351 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
352 $discussionObj = (object) $chatRow;
353 $this->generate_title_for_discussion( $discussionObj );
354 $counter++;
355 }
356 }
357 // If you want the newly-updated titles to show up *immediately*:
358 $chats = $this->chats_query( [], $offset, $limit, $filters );
359 }
360 // END NEW CHECK
361
362 // Apply filters to discussion metadata
363 foreach ( $chats['rows'] as &$chatRow ) {
364 // Decode messages JSON to get the count
365 $messages = json_decode( $chatRow['messages'], true );
366 $message_count = is_array( $messages ) ? count( $messages ) : 0;
367
368 // Add formatted metadata that can be filtered
369 $chatRow['metadata_display'] = [
370 'start_date' => apply_filters( 'mwai_discussion_metadata_start_date', $this->core->format_discussion_date( $chatRow['created'] ), $chatRow ),
371 'last_update' => apply_filters( 'mwai_discussion_metadata_last_update', $this->core->format_discussion_date( $chatRow['updated'] ), $chatRow ),
372 'message_count' => apply_filters( 'mwai_discussion_metadata_message_count', $message_count, $chatRow )
373 ];
374 }
375
376 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
377 }
378 catch ( Exception $e ) {
379 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
380 }
381 }
382
383 public function rest_discussions_delete_admin( $request ) {
384 try {
385 $params = $request->get_json_params();
386 $chatsIds = $params['chatIds'];
387 if ( is_array( $chatsIds ) ) {
388 if ( count( $chatsIds ) === 0 ) {
389 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
390 }
391 foreach ( $chatsIds as $chatId ) {
392 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
393 }
394 }
395 return $this->create_rest_response( [ 'success' => true ], 200 );
396 }
397 catch ( Exception $e ) {
398 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
399 }
400 }
401
402 public function rest_discussions_delete( $request ) {
403 try {
404 $params = $request->get_json_params();
405 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
406
407 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
408 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
409 }
410
411 $userId = get_current_user_id();
412 if ( !$userId ) {
413 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
414 }
415
416 foreach ( $chatIds as $chatId ) {
417 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
418 }
419
420 return $this->create_rest_response( [ 'success' => true ], 200 );
421 }
422 catch ( Exception $e ) {
423 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
424 }
425 }
426
427 // Get latest discussion for the given parameter
428 public function get_discussion( $botId, $chatId ) {
429 $this->check_db();
430 $chat = $this->wpdb->get_row(
431 $this->wpdb->prepare(
432 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
433 $chatId,
434 $botId
435 ),
436 ARRAY_A
437 );
438 if ( $chat ) {
439 $chat['messages'] = json_decode( $chat['messages'] );
440 return $chat;
441 }
442 return null;
443 }
444
445 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
446 $this->check_db();
447 $offset = !empty( $offset ) ? intval( $offset ) : 0;
448 $limit = !empty( $limit ) ? intval( $limit ) : 5;
449 $filters = !empty( $filters ) ? $filters : [];
450 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
451
452 $where_clauses = [];
453 $where_values = [];
454
455 if ( is_array( $filters ) ) {
456 foreach ( $filters as $filter ) {
457 $value = $filter['value'];
458 if ( is_null( $value ) || $value === '' ) {
459 continue;
460 }
461 switch ( $filter['accessor'] ) {
462 case 'user':
463 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
464 if ( $isIP ) {
465 $where_clauses[] = 'ip = %s';
466 $where_values[] = $value;
467 }
468 else {
469 $where_clauses[] = 'userId = %d';
470 $where_values[] = intval( $value );
471 }
472 break;
473 case 'botId':
474 $where_clauses[] = 'botId = %s';
475 $where_values[] = $value;
476 break;
477 case 'preview':
478 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
479 $where_clauses[] = 'messages LIKE %s';
480 $where_values[] = $like;
481 break;
482 // Add other cases as needed
483 }
484 }
485 }
486
487 $where_sql = '';
488 if ( !empty( $where_clauses ) ) {
489 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
490 }
491 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
492
493 $limit_sql = '';
494 if ( $limit > 0 ) {
495 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
496 }
497
498 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
499 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
500
501 // Get the total count
502 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
503 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
504
505 return $chats;
506 }
507
508 public function chatbot_reply( $rawText, $query, $params, $extra ) {
509 global $mwai_core;
510 $userIp = $mwai_core->get_ip_address();
511 $userId = $mwai_core->get_user_id();
512 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
513 $chatId = $this->core->fix_chat_id( $query, $params );
514 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
515 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
516 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
517 $now = date( 'Y-m-d H:i:s' );
518
519 if ( !empty( $customId ) ) {
520 $botId = $customId;
521 }
522 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
523
524 // If there is a file for "Vision", add it to the message
525 if ( isset( $query->filePurpose ) && $query->filePurpose === 'vision' && isset( $query->file ) ) {
526 $newMessage = "![Uploaded Image]({$query->file})\n" . $newMessage;
527 }
528
529 $this->check_db();
530 $chat = $this->wpdb->get_row(
531 $this->wpdb->prepare(
532 "SELECT * FROM $this->table_chats WHERE chatId = %s",
533 $chatId
534 )
535 );
536 $messageExtra = [
537 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
538 ];
539 $chatExtra = [
540 'session' => $query->session,
541 'model' => $query->model,
542 ];
543 if ( !empty( $query->temperature ) ) {
544 $chatExtra['temperature'] = $query->temperature;
545 }
546 if ( !empty( $query->context ) ) {
547 $chatExtra['context'] = $query->context;
548 }
549 if ( !empty( $params['parentBotId'] ) ) {
550 $chatExtra['parentBotId'] = $params['parentBotId'];
551 }
552 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
553 $chatExtra['assistantId'] = $query->assistantId;
554 $chatExtra['threadId'] = $query->threadId;
555 $chatExtra['storeId'] = $query->storeId;
556 }
557
558 // Store response ID and date for Responses API
559 if ( !empty( $extra['responseId'] ) ) {
560 $chatExtra['previousResponseId'] = $extra['responseId'];
561 $chatExtra['previousResponseDate'] = $now;
562 }
563
564 if ( $chat ) {
565 $chat->messages = json_decode( $chat->messages );
566 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
567 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
568 $chat->messages = json_encode( $chat->messages );
569
570 // Update or merge extra data
571 $existingExtra = json_decode( $chat->extra, true ) ?: [];
572 $mergedExtra = array_merge( $existingExtra, $chatExtra );
573
574 $this->wpdb->update(
575 $this->table_chats,
576 [
577 'userId' => $userId,
578 'messages' => $chat->messages,
579 'extra' => json_encode( $mergedExtra ),
580 'updated' => $now
581 ],
582 [ 'id' => $chat->id ]
583 );
584 }
585 else {
586 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
587 $messages = [];
588 if ( !empty( $startSentence ) ) {
589 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
590 }
591 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
592 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
593 $chat = [
594 'userId' => $userId,
595 'ip' => $userIp,
596 'messages' => json_encode( $messages ),
597 'extra' => json_encode( $chatExtra ),
598 'botId' => $botId,
599 'chatId' => $chatId,
600 'threadId' => $threadId,
601 'storeId' => $storeId,
602 'created' => $now,
603 'updated' => $now
604 ];
605 $this->wpdb->insert( $this->table_chats, $chat );
606 }
607 return $rawText;
608 }
609
610 public function format_messages( $json, $format = 'html' ) {
611 $html = '';
612 if ( $format === 'html' ) {
613 try {
614 $conversation = json_decode( $json, true );
615 if ( json_last_error() !== JSON_ERROR_NONE ) {
616 return 'Invalid JSON format';
617 }
618 foreach ( $conversation as $message ) {
619 $role = ucfirst( $message['role'] );
620 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
621 }
622 }
623 catch ( Exception $e ) {
624 error_log( $e->getMessage() );
625 return 'Error while formatting the message';
626 }
627 }
628 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
629 return $html;
630 }
631
632 /**
633 * Commits a discussion into the database (create or update if the same chatId is found).
634 *
635 * @param Meow_MWAI_Discussion $discussionObject
636 * @return bool True if success, false if error
637 */
638 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
639 $this->check_db();
640
641 // 1. Check if a discussion with the same chatId already exists
642 $chat = $this->wpdb->get_row(
643 $this->wpdb->prepare(
644 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
645 $discussionObject->chatId
646 ),
647 ARRAY_A
648 );
649
650 // 2. Prepare data for DB
651 $userIp = $this->core->get_ip_address();
652 $userId = $this->core->get_user_id();
653 $now = date( 'Y-m-d H:i:s' );
654
655 $data = [
656 'userId' => $userId,
657 'ip' => $userIp,
658 'botId' => $discussionObject->botId,
659 'chatId' => $discussionObject->chatId,
660 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
661 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
662 'updated' => $now,
663 ];
664
665 // 3. Update if found, otherwise insert a new row
666 if ( $chat ) {
667 $updateRes = $this->wpdb->update(
668 $this->table_chats,
669 $data,
670 [ 'id' => $chat['id'] ]
671 );
672 if ( $updateRes === false ) {
673 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
674 return false;
675 }
676 }
677 else {
678 // For insertion, also set "created"
679 $data['created'] = $now;
680 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
681 if ( $insertRes === false ) {
682 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
683 return false;
684 }
685 }
686
687 return true;
688 }
689
690 public function check_db() {
691 if ( $this->db_check ) {
692 return true;
693 }
694 $this->db_check = !(
695 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
696 != strtolower( $this->table_chats )
697 );
698 if ( !$this->db_check ) {
699 $this->create_db();
700 $this->db_check = !(
701 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
702 != strtolower( $this->table_chats )
703 );
704 }
705
706 // LATER: REMOVE THIS AFTER MARCH 2025
707 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_chats LIKE 'title'" );
708 // if ( ! $this->db_check ) {
709 // $this->wpdb->query( "ALTER TABLE $this->table_chats ADD COLUMN title VARCHAR(64) NULL" );
710 // $this->db_check = true;
711 // }
712
713 // LATER: REMOVE THIS AFTER SEPTEMBER 2025
714 // Migrate guest users from userId = 0 to userId = NULL
715 $guest_count = $this->wpdb->get_var( "SELECT COUNT(*) FROM $this->table_chats WHERE userId = 0" );
716 if ( $guest_count > 0 ) {
717 $this->wpdb->query( "UPDATE $this->table_chats SET userId = NULL WHERE userId = 0" );
718 }
719
720 return $this->db_check;
721 }
722
723 public function create_db() {
724 $charset_collate = $this->wpdb->get_charset_collate();
725 $sqlLogs = "CREATE TABLE $this->table_chats (
726 id BIGINT(20) NOT NULL AUTO_INCREMENT,
727 userId BIGINT(20) NULL,
728 ip VARCHAR(64) NULL,
729 title VARCHAR(64) NULL,
730 messages TEXT NOT NULL NULL,
731 extra LONGTEXT NOT NULL NULL,
732 botId VARCHAR(64) NULL,
733 chatId VARCHAR(64) NOT NULL,
734 threadId VARCHAR(64) NULL,
735 storeId VARCHAR(64) NULL,
736 created DATETIME NOT NULL,
737 updated DATETIME NOT NULL,
738 PRIMARY KEY (id),
739 INDEX chatId (chatId)
740 ) $charset_collate;";
741 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
742 dbDelta( $sqlLogs );
743 }
744 }
745