PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.7
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 11 months ago chatbot.php 11 months ago discussions.php 11 months ago files.php 11 months ago gdpr.php 11 months ago search.php 11 months ago security.php 11 months ago tasks.php 11 months ago wand.php 11 months ago
discussions.php
718 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 if ( $this->core->get_option( 'debug_mode' ) ) {
110 error_log( '[MWAI] Token refresh: Nonce is 12-24 hours old, providing fresh token' );
111 }
112 }
113 }
114
115 // If the nonce has changed or should be refreshed, include the new one
116 if ( $should_refresh || ( $request_nonce && $current_nonce !== $request_nonce ) ) {
117 $data['new_token'] = $current_nonce;
118
119 // Log if debug mode is enabled
120 if ( $this->core->get_option( 'debug_mode' ) ) {
121 error_log( '[MWAI] Token refresh: Sending new token in response' );
122 }
123 }
124
125 return new WP_REST_Response( $data, $status );
126 }
127
128 /**
129 * Generate or update the title for a specific discussion
130 * by calling the AI (if it meets the requirements).
131 *
132 * @param stdClass $discussion A row from the DB (object form).
133 * @return void
134 */
135 private function generate_title_for_discussion( $discussion ) {
136 // Check if there's already a title
137 if ( !empty( $discussion->title ) ) {
138 return; // Nothing to do if title is already set.
139 }
140
141 // Ensure it's not older than 10 days, or whatever logic you prefer
142 $ten_days_ago = strtotime( '-10 days' );
143 if ( strtotime( $discussion->updated ) < $ten_days_ago ) {
144 return; // Skip if older than 10 days
145 }
146
147 // We expect JSON in the messages
148 $messages = json_decode( $discussion->messages, true );
149 if ( !is_array( $messages ) ) {
150 return;
151 }
152
153 // Check for at least one user and one assistant message
154 $has_user_message = false;
155 $has_assistant_message = false;
156 foreach ( $messages as $message ) {
157 if ( isset( $message['role'] ) ) {
158 if ( $message['role'] === 'user' ) {
159 $has_user_message = true;
160 }
161 if ( $message['role'] === 'assistant' ) {
162 $has_assistant_message = true;
163 }
164 }
165 if ( $has_user_message && $has_assistant_message ) {
166 break;
167 }
168 }
169
170 if ( !( $has_user_message && $has_assistant_message ) ) {
171 return; // If doesn't have both, skip
172 }
173
174 // Prepare the conversation text for the prompt
175 $conversation_text = '';
176 foreach ( $messages as $message ) {
177 if ( isset( $message['role'] ) && isset( $message['content'] ) ) {
178 $role = ucfirst( $message['role'] );
179 $content = $message['content'];
180 $conversation_text .= "$role: $content\n";
181 }
182 }
183
184 $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";
185 $prompt = apply_filters( 'mwai_discussions_title_prompt', $base_prompt, $conversation_text, $discussion );
186
187 // Run the AI query using the fast environment
188 global $mwai;
189 $answer = $mwai->simpleTextQuery( $prompt, [
190 'scope' => 'discussions',
191 'envId' => $this->core->get_option( 'ai_fast_default_env' ),
192 'model' => $this->core->get_option( 'ai_fast_default_model' ),
193 ] );
194
195 // Clean up the answer
196 $title = trim( $answer );
197 $title = rtrim( $title, '.!?:;,—–-–' ); // Remove trailing punctuation
198 $title = substr( $title, 0, 64 ); // Ensure less than 64 characters
199 if ( empty( $title ) ) {
200 $title = 'Untitled';
201 }
202
203 // Update the discussion with the title
204 $updated = $this->wpdb->update(
205 $this->table_chats,
206 [ 'title' => $title ],
207 [ 'id' => $discussion->id ]
208 );
209 if ( $updated === false ) {
210 error_log( "Failed to update the title for discussion ID {$discussion->id}" );
211 }
212 }
213
214 /**
215 * Admin route for listing discussions. No forced logic here.
216 */
217 public function rest_discussions_list( $request ) {
218 try {
219 $params = $request->get_json_params();
220 $offset = $params['offset'];
221 $limit = $params['limit'];
222 $filters = $params['filters'];
223 $sort = $params['sort'];
224
225 // Retrieve the chats
226 $chats = $this->chats_query( [], $offset, $limit, $filters, $sort );
227
228 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
229 }
230 catch ( Exception $e ) {
231 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
232 }
233 }
234
235 public function rest_discussions_ui_edit( $request ) {
236 try {
237 $params = $request->get_json_params();
238 $chatId = isset( $params['chatId'] ) ? sanitize_text_field( $params['chatId'] ) : null;
239 $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : null;
240
241 if ( is_null( $chatId ) || is_null( $title ) ) {
242 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatId and title are required.' ], 400 );
243 }
244
245 $userId = get_current_user_id();
246 if ( !$userId ) {
247 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
248 }
249
250 // Update the discussion title for the current user
251 $updated = $this->wpdb->update(
252 $this->table_chats,
253 [ 'title' => $title ],
254 [ 'chatId' => $chatId, 'userId' => $userId ]
255 );
256 if ( $updated === false ) {
257 return $this->create_rest_response( [ 'success' => false, 'message' => 'Failed to update the discussion.' ], 500 );
258 }
259
260 return $this->create_rest_response( [ 'success' => true ], 200 );
261 }
262 catch ( Exception $e ) {
263 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
264 }
265 }
266
267 public function cron_discussions() {
268 $this->check_db();
269
270 // NEW CHECK: Only run if auto-titling is enabled
271 if ( !$this->core->get_option( 'chatbot_discussions_titling' ) ) {
272 return;
273 }
274 // END NEW CHECK
275
276 $now = date( 'Y-m-d H:i:s' );
277 $ten_days_ago = date( 'Y-m-d H:i:s', strtotime( '-10 days' ) );
278
279 // Get 5 latest discussions, not older than 10 days, which have no 'title' yet
280 $query = $this->wpdb->prepare(
281 "SELECT * FROM {$this->table_chats}
282 WHERE title IS NULL AND updated >= %s
283 ORDER BY updated DESC LIMIT 5",
284 $ten_days_ago
285 );
286 $discussions = $this->wpdb->get_results( $query );
287 if ( empty( $discussions ) ) {
288 return;
289 }
290
291 foreach ( $discussions as $discussion ) {
292 $this->generate_title_for_discussion( $discussion );
293 }
294 }
295
296 /**
297 * UI route for listing discussions.
298 * Here we add the "forced cron" logic for up to 5 discussions,
299 * but only if auto-titling is enabled.
300 */
301 public function rest_discussions_ui_list( $request ) {
302 try {
303 $params = $request->get_json_params();
304 $offset = isset( $params['offset'] ) ? $params['offset'] : 0;
305 // Get paging setting from options
306 $paging_option = $this->core->get_option( 'chatbot_discussions_paging' );
307 if ( $paging_option === 'None' ) {
308 $default_limit = 999; // Show all discussions
309 }
310 else {
311 $default_limit = is_numeric( $paging_option ) ? intval( $paging_option ) : 10; // Fallback to 10
312 }
313 $limit = isset( $params['limit'] ) ? $params['limit'] : $default_limit;
314 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
315 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
316
317 if ( !is_null( $customId ) ) {
318 $botId = $customId;
319 }
320 if ( is_null( $botId ) ) {
321 return $this->create_rest_response( [ 'success' => false, 'message' => 'Bot ID is required.' ], 200 );
322 }
323
324 $userId = get_current_user_id();
325 if ( !$userId ) {
326 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be connected.' ], 200 );
327 }
328
329 $filters = [
330 [ 'accessor' => 'user', 'value' => $userId ],
331 [ 'accessor' => 'botId', 'value' => $botId ],
332 ];
333
334 // Retrieve the chats
335 $chats = $this->chats_query( [], $offset, $limit, $filters );
336
337 // NEW CHECK: only do forced titling if it's enabled
338 if ( $this->core->get_option( 'chatbot_discussions_titling' ) ) {
339 // "Forced cron" logic: check up to 5 that have no title
340 $counter = 0;
341 foreach ( $chats['rows'] as &$chatRow ) {
342 if ( $counter >= 5 ) {
343 break;
344 }
345 if ( empty( $chatRow['title'] ) && strtotime( $chatRow['updated'] ) >= strtotime( '-10 days' ) ) {
346 $discussionObj = (object) $chatRow;
347 $this->generate_title_for_discussion( $discussionObj );
348 $counter++;
349 }
350 }
351 // If you want the newly-updated titles to show up *immediately*:
352 $chats = $this->chats_query( [], $offset, $limit, $filters );
353 }
354 // END NEW CHECK
355
356 return $this->create_rest_response( [ 'success' => true, 'total' => $chats['total'], 'chats' => $chats['rows'] ], 200 );
357 }
358 catch ( Exception $e ) {
359 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
360 }
361 }
362
363 public function rest_discussions_delete_admin( $request ) {
364 try {
365 $params = $request->get_json_params();
366 $chatsIds = $params['chatIds'];
367 if ( is_array( $chatsIds ) ) {
368 if ( count( $chatsIds ) === 0 ) {
369 $this->wpdb->query( "TRUNCATE TABLE $this->table_chats" );
370 }
371 foreach ( $chatsIds as $chatId ) {
372 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId ] );
373 }
374 }
375 return $this->create_rest_response( [ 'success' => true ], 200 );
376 }
377 catch ( Exception $e ) {
378 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
379 }
380 }
381
382 public function rest_discussions_delete( $request ) {
383 try {
384 $params = $request->get_json_params();
385 $chatIds = isset( $params['chatIds'] ) ? $params['chatIds'] : null;
386
387 if ( !is_array( $chatIds ) || empty( $chatIds ) ) {
388 return $this->create_rest_response( [ 'success' => false, 'message' => 'chatIds is required.' ], 400 );
389 }
390
391 $userId = get_current_user_id();
392 if ( !$userId ) {
393 return $this->create_rest_response( [ 'success' => false, 'message' => 'You need to be logged in.' ], 401 );
394 }
395
396 foreach ( $chatIds as $chatId ) {
397 $this->wpdb->delete( $this->table_chats, [ 'chatId' => $chatId, 'userId' => $userId ] );
398 }
399
400 return $this->create_rest_response( [ 'success' => true ], 200 );
401 }
402 catch ( Exception $e ) {
403 return $this->create_rest_response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
404 }
405 }
406
407 // Get latest discussion for the given parameter
408 public function get_discussion( $botId, $chatId ) {
409 $this->check_db();
410 $chat = $this->wpdb->get_row(
411 $this->wpdb->prepare(
412 "SELECT * FROM $this->table_chats WHERE chatId = %s AND botId = %s",
413 $chatId,
414 $botId
415 ),
416 ARRAY_A
417 );
418 if ( $chat ) {
419 $chat['messages'] = json_decode( $chat['messages'] );
420 return $chat;
421 }
422 return null;
423 }
424
425 public function chats_query( $chats = [], $offset = 0, $limit = null, $filters = null, $sort = null ) {
426 $this->check_db();
427 $offset = !empty( $offset ) ? intval( $offset ) : 0;
428 $limit = !empty( $limit ) ? intval( $limit ) : 5;
429 $filters = !empty( $filters ) ? $filters : [];
430 $this->core->sanitize_sort( $sort, 'updated', 'DESC' );
431
432 $where_clauses = [];
433 $where_values = [];
434
435 if ( is_array( $filters ) ) {
436 foreach ( $filters as $filter ) {
437 $value = $filter['value'];
438 if ( is_null( $value ) || $value === '' ) {
439 continue;
440 }
441 switch ( $filter['accessor'] ) {
442 case 'user':
443 $isIP = filter_var( $value, FILTER_VALIDATE_IP );
444 if ( $isIP ) {
445 $where_clauses[] = 'ip = %s';
446 $where_values[] = $value;
447 }
448 else {
449 $where_clauses[] = 'userId = %d';
450 $where_values[] = intval( $value );
451 }
452 break;
453 case 'botId':
454 $where_clauses[] = 'botId = %s';
455 $where_values[] = $value;
456 break;
457 case 'preview':
458 $like = '%' . $this->wpdb->esc_like( $value ) . '%';
459 $where_clauses[] = 'messages LIKE %s';
460 $where_values[] = $like;
461 break;
462 // Add other cases as needed
463 }
464 }
465 }
466
467 $where_sql = '';
468 if ( !empty( $where_clauses ) ) {
469 $where_sql = 'WHERE ' . implode( ' AND ', $where_clauses );
470 }
471 $order_by = 'ORDER BY ' . esc_sql( $sort['accessor'] ) . ' ' . esc_sql( $sort['by'] );
472
473 $limit_sql = '';
474 if ( $limit > 0 ) {
475 $limit_sql = $this->wpdb->prepare( 'LIMIT %d, %d', $offset, $limit );
476 }
477
478 $query = "SELECT * FROM {$this->table_chats} {$where_sql} {$order_by} {$limit_sql}";
479 $chats['rows'] = $this->wpdb->get_results( $this->wpdb->prepare( $query, $where_values ), ARRAY_A );
480
481 // Get the total count
482 $count_query = "SELECT COUNT(*) FROM {$this->table_chats} {$where_sql}";
483 $chats['total'] = $this->wpdb->get_var( $this->wpdb->prepare( $count_query, $where_values ) );
484
485 return $chats;
486 }
487
488 public function chatbot_reply( $rawText, $query, $params, $extra ) {
489 global $mwai_core;
490 $userIp = $mwai_core->get_ip_address();
491 $userId = $mwai_core->get_user_id();
492 $botId = isset( $params['botId'] ) ? $params['botId'] : null;
493 $chatId = $this->core->fix_chat_id( $query, $params );
494 $customId = isset( $params['customId'] ) ? $params['customId'] : null;
495 $threadId = $query instanceof Meow_MWAI_Query_Assistant ? $query->threadId : null;
496 $storeId = $query instanceof Meow_MWAI_Query_Assistant ? $query->storeId : null;
497 $now = date( 'Y-m-d H:i:s' );
498
499 if ( !empty( $customId ) ) {
500 $botId = $customId;
501 }
502 $newMessage = isset( $params['newMessage'] ) ? $params['newMessage'] : $query->get_message();
503
504 // If there is a file for "Vision", add it to the message
505 if ( isset( $query->filePurpose ) && $query->filePurpose === 'vision' && isset( $query->file ) ) {
506 $newMessage = "![Uploaded Image]({$query->file})\n" . $newMessage;
507 }
508
509 $this->check_db();
510 $chat = $this->wpdb->get_row(
511 $this->wpdb->prepare(
512 "SELECT * FROM $this->table_chats WHERE chatId = %s",
513 $chatId
514 )
515 );
516 $messageExtra = [
517 'embeddings' => isset( $extra['embeddings'] ) ? $extra['embeddings'] : null
518 ];
519 $chatExtra = [
520 'session' => $query->session,
521 'model' => $query->model,
522 ];
523 if ( !empty( $query->temperature ) ) {
524 $chatExtra['temperature'] = $query->temperature;
525 }
526 if ( !empty( $query->context ) ) {
527 $chatExtra['context'] = $query->context;
528 }
529 if ( !empty( $params['parentBotId'] ) ) {
530 $chatExtra['parentBotId'] = $params['parentBotId'];
531 }
532 if ( $query instanceof Meow_MWAI_Query_Assistant ) {
533 $chatExtra['assistantId'] = $query->assistantId;
534 $chatExtra['threadId'] = $query->threadId;
535 $chatExtra['storeId'] = $query->storeId;
536 }
537
538 // Store response ID and date for Responses API
539 if ( !empty( $extra['responseId'] ) ) {
540 $chatExtra['previousResponseId'] = $extra['responseId'];
541 $chatExtra['previousResponseDate'] = $now;
542 }
543
544 if ( $chat ) {
545 $chat->messages = json_decode( $chat->messages );
546 $chat->messages[] = [ 'role' => 'user', 'content' => $newMessage ];
547 $chat->messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
548 $chat->messages = json_encode( $chat->messages );
549
550 // Update or merge extra data
551 $existingExtra = json_decode( $chat->extra, true ) ?: [];
552 $mergedExtra = array_merge( $existingExtra, $chatExtra );
553
554 $this->wpdb->update(
555 $this->table_chats,
556 [
557 'userId' => $userId,
558 'messages' => $chat->messages,
559 'extra' => json_encode( $mergedExtra ),
560 'updated' => $now
561 ],
562 [ 'id' => $chat->id ]
563 );
564 }
565 else {
566 $startSentence = isset( $params['startSentence'] ) ? $params['startSentence'] : null;
567 $messages = [];
568 if ( !empty( $startSentence ) ) {
569 $messages[] = [ 'role' => 'assistant', 'content' => $startSentence ];
570 }
571 $messages[] = [ 'role' => 'user', 'content' => $newMessage ];
572 $messages[] = [ 'role' => 'assistant', 'content' => $rawText, 'extra' => $messageExtra ];
573 $chat = [
574 'userId' => $userId,
575 'ip' => $userIp,
576 'messages' => json_encode( $messages ),
577 'extra' => json_encode( $chatExtra ),
578 'botId' => $botId,
579 'chatId' => $chatId,
580 'threadId' => $threadId,
581 'storeId' => $storeId,
582 'created' => $now,
583 'updated' => $now
584 ];
585 $this->wpdb->insert( $this->table_chats, $chat );
586 }
587 return $rawText;
588 }
589
590 public function format_messages( $json, $format = 'html' ) {
591 $html = '';
592 if ( $format === 'html' ) {
593 try {
594 $conversation = json_decode( $json, true );
595 if ( json_last_error() !== JSON_ERROR_NONE ) {
596 return 'Invalid JSON format';
597 }
598 foreach ( $conversation as $message ) {
599 $role = ucfirst( $message['role'] );
600 $html .= '<p><strong>' . htmlspecialchars( $role ) . ':</strong> ' . htmlspecialchars( $message['content'] ) . '</p>';
601 }
602 }
603 catch ( Exception $e ) {
604 error_log( $e->getMessage() );
605 return 'Error while formatting the message';
606 }
607 }
608 $html = apply_filters( 'mwai_discussion_format_messages', $html, $json, $format );
609 return $html;
610 }
611
612 /**
613 * Commits a discussion into the database (create or update if the same chatId is found).
614 *
615 * @param Meow_MWAI_Discussion $discussionObject
616 * @return bool True if success, false if error
617 */
618 public function commit_discussion( Meow_MWAI_Discussion $discussionObject ): bool {
619 $this->check_db();
620
621 // 1. Check if a discussion with the same chatId already exists
622 $chat = $this->wpdb->get_row(
623 $this->wpdb->prepare(
624 "SELECT * FROM {$this->table_chats} WHERE chatId = %s",
625 $discussionObject->chatId
626 ),
627 ARRAY_A
628 );
629
630 // 2. Prepare data for DB
631 $userIp = $this->core->get_ip_address();
632 $userId = $this->core->get_user_id();
633 $now = date( 'Y-m-d H:i:s' );
634
635 $data = [
636 'userId' => $userId,
637 'ip' => $userIp,
638 'botId' => $discussionObject->botId,
639 'chatId' => $discussionObject->chatId,
640 'messages' => !empty( $discussionObject->messages ) ? wp_json_encode( $discussionObject->messages ) : '[]',
641 'extra' => !empty( $discussionObject->extra ) ? wp_json_encode( $discussionObject->extra ) : '{}',
642 'updated' => $now,
643 ];
644
645 // 3. Update if found, otherwise insert a new row
646 if ( $chat ) {
647 $updateRes = $this->wpdb->update(
648 $this->table_chats,
649 $data,
650 [ 'id' => $chat['id'] ]
651 );
652 if ( $updateRes === false ) {
653 error_log( 'Error updating discussion: ' . $this->wpdb->last_error );
654 return false;
655 }
656 }
657 else {
658 // For insertion, also set "created"
659 $data['created'] = $now;
660 $insertRes = $this->wpdb->insert( $this->table_chats, $data );
661 if ( $insertRes === false ) {
662 error_log( 'Error inserting discussion: ' . $this->wpdb->last_error );
663 return false;
664 }
665 }
666
667 return true;
668 }
669
670 public function check_db() {
671 if ( $this->db_check ) {
672 return true;
673 }
674 $this->db_check = !(
675 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
676 != strtolower( $this->table_chats )
677 );
678 if ( !$this->db_check ) {
679 $this->create_db();
680 $this->db_check = !(
681 strtolower( $this->wpdb->get_var( "SHOW TABLES LIKE '$this->table_chats'" ) )
682 != strtolower( $this->table_chats )
683 );
684 }
685
686 // LATER: REMOVE THIS AFTER MARCH 2025
687 // $this->db_check = $this->db_check && $this->wpdb->get_var( "SHOW COLUMNS FROM $this->table_chats LIKE 'title'" );
688 // if ( ! $this->db_check ) {
689 // $this->wpdb->query( "ALTER TABLE $this->table_chats ADD COLUMN title VARCHAR(64) NULL" );
690 // $this->db_check = true;
691 // }
692
693 return $this->db_check;
694 }
695
696 public function create_db() {
697 $charset_collate = $this->wpdb->get_charset_collate();
698 $sqlLogs = "CREATE TABLE $this->table_chats (
699 id BIGINT(20) NOT NULL AUTO_INCREMENT,
700 userId BIGINT(20) NULL,
701 ip VARCHAR(64) NULL,
702 title VARCHAR(64) NULL,
703 messages TEXT NOT NULL NULL,
704 extra LONGTEXT NOT NULL NULL,
705 botId VARCHAR(64) NULL,
706 chatId VARCHAR(64) NOT NULL,
707 threadId VARCHAR(64) NULL,
708 storeId VARCHAR(64) NULL,
709 created DATETIME NOT NULL,
710 updated DATETIME NOT NULL,
711 PRIMARY KEY (id),
712 INDEX chatId (chatId)
713 ) $charset_collate;";
714 require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
715 dbDelta( $sqlLogs );
716 }
717 }
718