data
1 year ago
engines
10 months ago
exceptions
1 year ago
modules
10 months ago
query
10 months ago
rest
10 months ago
services
10 months ago
admin.php
10 months ago
api.php
10 months ago
core.php
10 months ago
discussion.php
1 year ago
event.php
1 year ago
init.php
11 months ago
logging.php
1 year ago
reply.php
10 months ago
rest.php
10 months ago
api.php
1239 lines
| 1 | <?php |
| 2 | |
| 3 | class Meow_MWAI_API { |
| 4 | public $core; |
| 5 | private $chatbot_module; |
| 6 | private $discussions_module; |
| 7 | private $bearer_token; |
| 8 | private $debug = false; |
| 9 | |
| 10 | public function __construct( $chatbot_module, $discussions_module ) { |
| 11 | global $mwai_core; |
| 12 | $this->core = $mwai_core; |
| 13 | $this->chatbot_module = $chatbot_module; |
| 14 | $this->discussions_module = $discussions_module; |
| 15 | add_action( 'rest_api_init', [ $this, 'rest_api_init' ] ); |
| 16 | $this->debug = $this->core->get_option( 'server_debug_mode' ); |
| 17 | } |
| 18 | |
| 19 | #region REST API |
| 20 | /** |
| 21 | * Sanitize REST API options to remove sensitive parameters |
| 22 | * that should not be controlled by untrusted users |
| 23 | */ |
| 24 | private function sanitize_rest_options( $options ) { |
| 25 | if ( !is_array( $options ) ) { |
| 26 | return []; |
| 27 | } |
| 28 | |
| 29 | // List of sensitive parameters that should not be set via REST API |
| 30 | $blocked_params = [ |
| 31 | 'apiKey', |
| 32 | ]; |
| 33 | $blocked_params = apply_filters( 'mwai_blocked_rest_params', $blocked_params, $options ); |
| 34 | |
| 35 | // Remove blocked parameters |
| 36 | foreach ( $blocked_params as $param ) { |
| 37 | unset( $options[$param] ); |
| 38 | } |
| 39 | |
| 40 | return $options; |
| 41 | } |
| 42 | |
| 43 | public function rest_api_init() { |
| 44 | $public_api = $this->core->get_option( 'public_api' ); |
| 45 | if ( !$public_api ) { |
| 46 | return; |
| 47 | } |
| 48 | $this->bearer_token = $this->core->get_option( 'public_api_bearer_token' ); |
| 49 | if ( !empty( $this->bearer_token ) ) { |
| 50 | add_filter( 'mwai_allow_public_api', [ $this, 'auth_via_bearer_token' ], 10, 3 ); |
| 51 | } |
| 52 | |
| 53 | register_rest_route( 'mwai/v1', '/simpleAuthCheck', [ |
| 54 | 'methods' => 'GET', |
| 55 | 'callback' => [ $this, 'rest_simpleAuthCheck' ], |
| 56 | 'permission_callback' => function ( $request ) { |
| 57 | return $this->core->can_access_public_api( 'simpleAuthCheck', $request ); |
| 58 | }, |
| 59 | ] ); |
| 60 | register_rest_route( 'mwai/v1', '/simpleTextQuery', [ |
| 61 | 'methods' => 'POST', |
| 62 | 'callback' => [ $this, 'rest_simpleTextQuery' ], |
| 63 | 'permission_callback' => function ( $request ) { |
| 64 | return $this->core->can_access_public_api( 'simpleTextQuery', $request ); |
| 65 | }, |
| 66 | ] ); |
| 67 | register_rest_route( 'mwai/v1', '/simpleFastTextQuery', [ |
| 68 | 'methods' => 'POST', |
| 69 | 'callback' => [ $this, 'rest_simpleFastTextQuery' ], |
| 70 | 'permission_callback' => function ( $request ) { |
| 71 | return $this->core->can_access_public_api( 'simpleFastTextQuery', $request ); |
| 72 | }, |
| 73 | ] ); |
| 74 | register_rest_route( 'mwai/v1', '/simpleImageQuery', [ |
| 75 | 'methods' => 'POST', |
| 76 | 'callback' => [ $this, 'rest_simpleImageQuery' ], |
| 77 | 'permission_callback' => function ( $request ) { |
| 78 | return $this->core->can_access_public_api( 'simpleImageQuery', $request ); |
| 79 | }, |
| 80 | ] ); |
| 81 | register_rest_route( 'mwai/v1', '/simpleImageEditQuery', [ |
| 82 | 'methods' => 'POST', |
| 83 | 'callback' => [ $this, 'rest_simpleImageEditQuery' ], |
| 84 | 'permission_callback' => function ( $request ) { |
| 85 | return $this->core->can_access_public_api( 'simpleImageEditQuery', $request ); |
| 86 | }, |
| 87 | ] ); |
| 88 | register_rest_route( 'mwai/v1', '/simpleVisionQuery', [ |
| 89 | 'methods' => 'POST', |
| 90 | 'callback' => [ $this, 'rest_simpleVisionQuery' ], |
| 91 | 'permission_callback' => function ( $request ) { |
| 92 | return $this->core->can_access_public_api( 'simpleVisionQuery', $request ); |
| 93 | }, |
| 94 | ] ); |
| 95 | register_rest_route( 'mwai/v1', '/simpleJsonQuery', [ |
| 96 | 'methods' => 'POST', |
| 97 | 'callback' => [ $this, 'rest_simpleJsonQuery' ], |
| 98 | 'permission_callback' => function ( $request ) { |
| 99 | return $this->core->can_access_public_api( 'simpleJsonQuery', $request ); |
| 100 | }, |
| 101 | ] ); |
| 102 | register_rest_route( 'mwai/v1', '/moderationCheck', [ |
| 103 | 'methods' => 'POST', |
| 104 | 'callback' => [ $this, 'rest_moderationCheck' ], |
| 105 | 'permission_callback' => function ( $request ) { |
| 106 | return $this->core->can_access_public_api( 'moderationCheck', $request ); |
| 107 | }, |
| 108 | ] ); |
| 109 | register_rest_route( 'mwai/v1', '/simpleTranscribeAudio', [ |
| 110 | 'methods' => 'POST', |
| 111 | 'callback' => [ $this, 'rest_simpleTranscribeAudio' ], |
| 112 | 'permission_callback' => function ( $request ) { |
| 113 | return $this->core->can_access_public_api( 'simpleTranscribeAudio', $request ); |
| 114 | }, |
| 115 | ] ); |
| 116 | register_rest_route( 'mwai/v1', '/simpleFileUpload', [ |
| 117 | 'methods' => 'POST', |
| 118 | 'callback' => [ $this, 'rest_simpleFileUpload' ], |
| 119 | 'permission_callback' => function ( $request ) { |
| 120 | return $this->core->can_access_public_api( 'simpleFileUpload', $request ); |
| 121 | }, |
| 122 | ] ); |
| 123 | |
| 124 | if ( $this->chatbot_module ) { |
| 125 | register_rest_route( 'mwai/v1', '/simpleChatbotQuery', [ |
| 126 | 'methods' => 'POST', |
| 127 | 'callback' => [ $this, 'rest_simpleChatbotQuery' ], |
| 128 | 'permission_callback' => function ( $request ) { |
| 129 | return $this->core->can_access_public_api( 'simpleChatbotQuery', $request ); |
| 130 | }, |
| 131 | ] ); |
| 132 | |
| 133 | register_rest_route( 'mwai/v1', '/listChatbots', [ |
| 134 | 'methods' => 'GET', |
| 135 | 'callback' => [ $this, 'rest_listChatbots' ], |
| 136 | 'permission_callback' => function ( $request ) { |
| 137 | return $this->core->can_access_public_api( 'listChatbots', $request ); |
| 138 | }, |
| 139 | ] ); |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | public function rest_simpleAuthCheck( $request ) { |
| 144 | try { |
| 145 | $params = $request->get_params(); |
| 146 | $current_user = wp_get_current_user(); |
| 147 | $current_email = $current_user->user_email; |
| 148 | return new WP_REST_Response( [ 'success' => true, 'data' => [ |
| 149 | 'type' => 'email', |
| 150 | 'value' => $current_email |
| 151 | ] ], 200 ); |
| 152 | } |
| 153 | catch ( Exception $e ) { |
| 154 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | public function auth_via_bearer_token( $allow, $feature, $extra ) { |
| 159 | if ( !empty( $extra ) && !empty( $extra->get_header( 'Authorization' ) ) ) { |
| 160 | $token = $extra->get_header( 'Authorization' ); |
| 161 | $token = str_replace( 'Bearer ', '', $token ); |
| 162 | if ( $token === $this->bearer_token ) { |
| 163 | // We set the current user to the first admin. |
| 164 | $admin = $this->core->get_admin_user(); |
| 165 | wp_set_current_user( $admin->ID, $admin->user_login ); |
| 166 | return true; |
| 167 | } |
| 168 | } |
| 169 | return $allow; |
| 170 | } |
| 171 | |
| 172 | public function rest_simpleChatbotQuery( $request ) { |
| 173 | try { |
| 174 | $params = $request->get_params(); |
| 175 | $botId = isset( $params['botId'] ) ? $params['botId'] : ''; |
| 176 | $message = isset( $params['message'] ) ? $params['message'] : ''; |
| 177 | if ( empty( $message ) ) { |
| 178 | $message = isset( $params['prompt'] ) ? $params['prompt'] : ''; |
| 179 | } |
| 180 | $chatId = isset( $params['chatId'] ) ? $params['chatId'] : null; |
| 181 | $fileId = isset( $params['fileId'] ) ? $params['fileId'] : null; |
| 182 | $queryParams = []; |
| 183 | if ( !empty( $chatId ) ) { |
| 184 | $queryParams['chatId'] = $chatId; |
| 185 | } |
| 186 | if ( !empty( $fileId ) ) { |
| 187 | $queryParams['fileId'] = $fileId; |
| 188 | } |
| 189 | if ( empty( $botId ) || empty( $message ) ) { |
| 190 | throw new Exception( 'The botId and message are required.' ); |
| 191 | } |
| 192 | |
| 193 | if ( $this->debug ) { |
| 194 | $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 ); |
| 195 | $debug = sprintf( 'REST [SimpleChatbotQuery]: %s, %s', $shortMessage, json_encode( $queryParams ) ); |
| 196 | Meow_MWAI_Logging::log( $debug ); |
| 197 | } |
| 198 | |
| 199 | $reply = $this->simpleChatbotQuery( $botId, $message, $queryParams, false ); |
| 200 | return new WP_REST_Response( [ |
| 201 | 'success' => true, |
| 202 | 'data' => $reply['reply'], |
| 203 | 'extra' => [ |
| 204 | 'actions' => $reply['actions'], |
| 205 | 'chatId' => $reply['chatId'] |
| 206 | ] |
| 207 | ], 200 ); |
| 208 | } |
| 209 | catch ( Exception $e ) { |
| 210 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | public function rest_listChatbots( $request ) { |
| 215 | try { |
| 216 | // Get all chatbots |
| 217 | $chatbots = get_option( 'mwai_chatbots', [] ); |
| 218 | $environments = $this->core->get_option( 'ai_envs' ); |
| 219 | $mcp_envs = $this->core->get_option( 'mcp_envs', [] ); |
| 220 | |
| 221 | // Get all models from all environments |
| 222 | $all_models = []; |
| 223 | foreach ( $environments as $env ) { |
| 224 | try { |
| 225 | $engine = Meow_MWAI_Engines_Factory::get( $this->core, $env['id'] ); |
| 226 | $env_models = $engine->retrieve_models(); |
| 227 | foreach ( $env_models as $model ) { |
| 228 | $all_models[$model['model']] = $model; |
| 229 | } |
| 230 | } |
| 231 | catch ( Exception $e ) { |
| 232 | // Skip environments that fail |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | // Debug: Log model info for gpt-4.1-mini |
| 237 | if ( $this->debug && isset( $all_models['gpt-4.1-mini'] ) ) { |
| 238 | error_log( '[AI Engine API] Model info for gpt-4.1-mini: ' . json_encode( $all_models['gpt-4.1-mini'] ) ); |
| 239 | } |
| 240 | |
| 241 | // Get registered functions |
| 242 | $functions = apply_filters( 'mwai_functions_list', [] ); |
| 243 | $function_names = []; |
| 244 | foreach ( $functions as $function ) { |
| 245 | $function_names[] = $function->name ?? 'unknown'; |
| 246 | } |
| 247 | |
| 248 | $result = []; |
| 249 | |
| 250 | foreach ( $chatbots as $chatbotId => $chatbot ) { |
| 251 | // Debug log the chatbot structure |
| 252 | if ( $this->debug && ( $chatbot['name'] ?? '' ) === 'Jordy' ) { |
| 253 | error_log( '[AI Engine API] Jordy chatbot structure: ' . json_encode( $chatbot ) ); |
| 254 | } |
| 255 | |
| 256 | // Basic info |
| 257 | $info = [ |
| 258 | 'id' => $chatbot['botId'] ?? $chatbotId, // Use botId if available, fallback to array index |
| 259 | 'name' => $chatbot['name'] ?? 'Unnamed', |
| 260 | 'type' => 'chat', // Default type |
| 261 | 'model' => null, |
| 262 | 'model_name' => null, |
| 263 | 'environment' => null, |
| 264 | 'environment_name' => null, |
| 265 | 'functions' => [], |
| 266 | 'tools' => [], // Add tools array |
| 267 | 'mcp_servers' => [] |
| 268 | ]; |
| 269 | |
| 270 | // Determine chatbot type |
| 271 | if ( !empty( $chatbot['type'] ) ) { |
| 272 | $info['type'] = $chatbot['type']; |
| 273 | } |
| 274 | else { |
| 275 | // Try to infer type from model or other properties |
| 276 | if ( !empty( $chatbot['model'] ) ) { |
| 277 | if ( strpos( $chatbot['model'], 'realtime' ) !== false ) { |
| 278 | $info['type'] = 'realtime'; |
| 279 | } |
| 280 | else if ( strpos( $chatbot['model'], 'image' ) !== false || strpos( $chatbot['model'], 'dall-e' ) !== false ) { |
| 281 | $info['type'] = 'images'; |
| 282 | } |
| 283 | else if ( !empty( $chatbot['assistantId'] ) ) { |
| 284 | $info['type'] = 'assistant'; |
| 285 | } |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | // Get model info |
| 290 | if ( !empty( $chatbot['model'] ) ) { |
| 291 | $info['model'] = $chatbot['model']; |
| 292 | // Find model name |
| 293 | if ( isset( $all_models[$chatbot['model']] ) ) { |
| 294 | $info['model_name'] = $all_models[$chatbot['model']]['name'] ?? $chatbot['model']; |
| 295 | } |
| 296 | else { |
| 297 | $info['model_name'] = $chatbot['model']; |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | // Get environment info |
| 302 | if ( !empty( $chatbot['envId'] ) ) { |
| 303 | $info['environment'] = $chatbot['envId']; |
| 304 | // Find environment name |
| 305 | foreach ( $environments as $env ) { |
| 306 | if ( $env['id'] === $chatbot['envId'] ) { |
| 307 | $info['environment_name'] = $env['name'] ?? $chatbot['envId']; |
| 308 | break; |
| 309 | } |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | // Check if it uses functions - get specific function names if available |
| 314 | if ( !empty( $chatbot['functions'] ) ) { |
| 315 | if ( is_array( $chatbot['functions'] ) ) { |
| 316 | // Functions are stored as array of objects with id and type |
| 317 | $chatbot_functions = []; |
| 318 | foreach ( $chatbot['functions'] as $func ) { |
| 319 | if ( isset( $func['id'] ) ) { |
| 320 | // Try to find function name by ID |
| 321 | $func_name = $this->get_function_name_by_id( $func['id'] ); |
| 322 | if ( $func_name ) { |
| 323 | $chatbot_functions[] = $func_name; |
| 324 | } |
| 325 | else { |
| 326 | // Fallback: include the ID if name not found |
| 327 | $chatbot_functions[] = 'function_' . $func['id']; |
| 328 | } |
| 329 | } |
| 330 | } |
| 331 | $info['functions'] = $chatbot_functions; |
| 332 | } |
| 333 | else if ( $chatbot['functions'] === true ) { |
| 334 | // If functions is just true, it uses all registered functions |
| 335 | $info['functions'] = $function_names; |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | // Check for tools (Web Search, Image Generation) |
| 340 | if ( !empty( $chatbot['tools'] ) && is_array( $chatbot['tools'] ) ) { |
| 341 | // Filter tools based on model capabilities |
| 342 | $supported_tools = []; |
| 343 | if ( !empty( $chatbot['model'] ) ) { |
| 344 | // Try exact match first |
| 345 | $model_key = $chatbot['model']; |
| 346 | $model_info = $all_models[$model_key] ?? null; |
| 347 | |
| 348 | // If not found and it's an OpenRouter model, try without prefix |
| 349 | if ( !$model_info && strpos( $model_key, '/' ) !== false ) { |
| 350 | $model_key = substr( $model_key, strpos( $model_key, '/' ) + 1 ); |
| 351 | $model_info = $all_models[$model_key] ?? null; |
| 352 | } |
| 353 | |
| 354 | if ( $model_info ) { |
| 355 | $model_tools = $model_info['tools'] ?? []; |
| 356 | |
| 357 | // Only include tools that are both configured AND supported by the model |
| 358 | foreach ( $chatbot['tools'] as $tool ) { |
| 359 | if ( in_array( $tool, $model_tools ) ) { |
| 360 | $supported_tools[] = $tool; |
| 361 | } |
| 362 | } |
| 363 | } |
| 364 | else { |
| 365 | // If model not found in our list, keep all configured tools |
| 366 | // This allows custom models to use tools |
| 367 | $supported_tools = $chatbot['tools']; |
| 368 | } |
| 369 | } |
| 370 | $info['tools'] = $supported_tools; |
| 371 | } |
| 372 | |
| 373 | // Check for MCP servers |
| 374 | if ( !empty( $chatbot['mcp_servers'] ) && is_array( $chatbot['mcp_servers'] ) ) { |
| 375 | foreach ( $chatbot['mcp_servers'] as $mcpServer ) { |
| 376 | if ( !empty( $mcpServer['id'] ) ) { |
| 377 | // Find MCP server name |
| 378 | foreach ( $mcp_envs as $mcp ) { |
| 379 | if ( $mcp['id'] === $mcpServer['id'] ) { |
| 380 | $info['mcp_servers'][] = [ |
| 381 | 'id' => $mcp['id'], |
| 382 | 'name' => $mcp['name'] ?? 'Unnamed MCP Server' |
| 383 | ]; |
| 384 | break; |
| 385 | } |
| 386 | } |
| 387 | } |
| 388 | } |
| 389 | } |
| 390 | |
| 391 | $result[] = $info; |
| 392 | } |
| 393 | |
| 394 | return new WP_REST_Response( [ |
| 395 | 'success' => true, |
| 396 | 'data' => $result |
| 397 | ], 200 ); |
| 398 | } |
| 399 | catch ( Exception $e ) { |
| 400 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 401 | } |
| 402 | } |
| 403 | |
| 404 | public function rest_simpleTextQuery( $request ) { |
| 405 | try { |
| 406 | $params = $request->get_params(); |
| 407 | $message = isset( $params['message'] ) ? $params['message'] : ''; |
| 408 | if ( empty( $message ) ) { |
| 409 | $message = isset( $params['prompt'] ) ? $params['prompt'] : ''; |
| 410 | } |
| 411 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 412 | $options = $this->sanitize_rest_options( $options ); |
| 413 | $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api'; |
| 414 | if ( !empty( $scope ) ) { |
| 415 | $options['scope'] = $scope; |
| 416 | } |
| 417 | if ( empty( $message ) ) { |
| 418 | throw new Exception( 'The message is required.' ); |
| 419 | } |
| 420 | |
| 421 | if ( $this->debug ) { |
| 422 | $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 ); |
| 423 | $debug = sprintf( 'REST [SimpleTextQuery]: %s, %s', $shortMessage, json_encode( $options ) ); |
| 424 | Meow_MWAI_Logging::log( $debug ); |
| 425 | } |
| 426 | |
| 427 | $reply = $this->simpleTextQuery( $message, $options ); |
| 428 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 429 | } |
| 430 | catch ( Exception $e ) { |
| 431 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 432 | } |
| 433 | } |
| 434 | |
| 435 | public function rest_simpleFastTextQuery( $request ) { |
| 436 | try { |
| 437 | $params = $request->get_params(); |
| 438 | $message = isset( $params['message'] ) ? $params['message'] : ''; |
| 439 | if ( empty( $message ) ) { |
| 440 | $message = isset( $params['prompt'] ) ? $params['prompt'] : ''; |
| 441 | } |
| 442 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 443 | $options = $this->sanitize_rest_options( $options ); |
| 444 | $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api'; |
| 445 | if ( !empty( $scope ) ) { |
| 446 | $options['scope'] = $scope; |
| 447 | } |
| 448 | if ( empty( $message ) ) { |
| 449 | throw new Exception( 'The message is required.' ); |
| 450 | } |
| 451 | |
| 452 | if ( $this->debug ) { |
| 453 | $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 ); |
| 454 | $debug = sprintf( 'REST [SimpleFastTextQuery]: %s, %s', $shortMessage, json_encode( $options ) ); |
| 455 | Meow_MWAI_Logging::log( $debug ); |
| 456 | } |
| 457 | |
| 458 | $reply = $this->simpleFastTextQuery( $message, $options ); |
| 459 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 460 | } |
| 461 | catch ( Exception $e ) { |
| 462 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 463 | } |
| 464 | } |
| 465 | |
| 466 | public function rest_simpleImageQuery( $request ) { |
| 467 | try { |
| 468 | $params = $request->get_params(); |
| 469 | $message = isset( $params['message'] ) ? $params['message'] : ''; |
| 470 | if ( empty( $message ) ) { |
| 471 | $message = isset( $params['prompt'] ) ? $params['prompt'] : ''; |
| 472 | } |
| 473 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 474 | $resolution = isset( $params['resolution'] ) ? $params['resolution'] : ''; |
| 475 | $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api'; |
| 476 | if ( !empty( $scope ) ) { |
| 477 | $options['scope'] = $scope; |
| 478 | } |
| 479 | if ( empty( $message ) ) { |
| 480 | throw new Exception( 'The message is required.' ); |
| 481 | } |
| 482 | if ( !empty( $resolution ) ) { |
| 483 | $options['resolution'] = $resolution; |
| 484 | } |
| 485 | |
| 486 | if ( $this->debug ) { |
| 487 | $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 ); |
| 488 | $debug = sprintf( 'REST [SimpleImageQuery]: %s, %s', $shortMessage, json_encode( $options ) ); |
| 489 | Meow_MWAI_Logging::log( $debug ); |
| 490 | } |
| 491 | |
| 492 | $reply = $this->simpleImageQuery( $message, $options ); |
| 493 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 494 | } |
| 495 | catch ( Exception $e ) { |
| 496 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 497 | } |
| 498 | } |
| 499 | |
| 500 | public function rest_simpleImageEditQuery( $request ) { |
| 501 | try { |
| 502 | $params = $request->get_params(); |
| 503 | $message = isset( $params['message'] ) ? $params['message'] : ''; |
| 504 | if ( empty( $message ) ) { |
| 505 | $message = isset( $params['prompt'] ) ? $params['prompt'] : ''; |
| 506 | } |
| 507 | $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0; |
| 508 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 509 | $resolution = isset( $params['resolution'] ) ? $params['resolution'] : ''; |
| 510 | $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api'; |
| 511 | if ( !empty( $scope ) ) { |
| 512 | $options['scope'] = $scope; |
| 513 | } |
| 514 | if ( empty( $message ) ) { |
| 515 | throw new Exception( 'The message is required.' ); |
| 516 | } |
| 517 | if ( empty( $mediaId ) ) { |
| 518 | throw new Exception( 'The mediaId is required.' ); |
| 519 | } |
| 520 | if ( !empty( $resolution ) ) { |
| 521 | $options['resolution'] = $resolution; |
| 522 | } |
| 523 | |
| 524 | if ( $this->debug ) { |
| 525 | $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 ); |
| 526 | $debug = sprintf( 'REST [SimpleImageEditQuery]: %s, %s', $shortMessage, json_encode( $options ) ); |
| 527 | Meow_MWAI_Logging::log( $debug ); |
| 528 | } |
| 529 | |
| 530 | $reply = $this->simpleImageEditQuery( $message, $mediaId, $options ); |
| 531 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 532 | } |
| 533 | catch ( Exception $e ) { |
| 534 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 535 | } |
| 536 | } |
| 537 | |
| 538 | public function rest_simpleVisionQuery( $request ) { |
| 539 | try { |
| 540 | $params = $request->get_params(); |
| 541 | $message = isset( $params['message'] ) ? $params['message'] : ''; |
| 542 | if ( empty( $message ) ) { |
| 543 | $message = isset( $params['prompt'] ) ? $params['prompt'] : ''; |
| 544 | } |
| 545 | $url = isset( $params['url'] ) ? $params['url'] : ''; |
| 546 | |
| 547 | // Check for common parameter mistakes and provide helpful guidance |
| 548 | if ( empty( $url ) && isset( $params['imageUrl'] ) ) { |
| 549 | throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "imageUrl"?' ); |
| 550 | } |
| 551 | if ( empty( $url ) && isset( $params['image_url'] ) ) { |
| 552 | throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "image_url"?' ); |
| 553 | } |
| 554 | |
| 555 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 556 | $options = $this->sanitize_rest_options( $options ); |
| 557 | $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api'; |
| 558 | if ( !empty( $scope ) ) { |
| 559 | $options['scope'] = $scope; |
| 560 | } |
| 561 | if ( empty( $message ) ) { |
| 562 | throw new Exception( 'The message is required.' ); |
| 563 | } |
| 564 | if ( empty( $url ) ) { |
| 565 | throw new Exception( 'The "url" parameter is required for image analysis.' ); |
| 566 | } |
| 567 | |
| 568 | if ( $this->debug ) { |
| 569 | $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 ); |
| 570 | $debug = sprintf( 'REST [SimpleVisionQuery]: %s, %s', $shortMessage, json_encode( $options ) ); |
| 571 | Meow_MWAI_Logging::log( $debug ); |
| 572 | } |
| 573 | |
| 574 | $reply = $this->simpleVisionQuery( $message, $url, null, $options ); |
| 575 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 576 | } |
| 577 | catch ( Exception $e ) { |
| 578 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 579 | } |
| 580 | } |
| 581 | |
| 582 | public function rest_simpleJsonQuery( $request ) { |
| 583 | try { |
| 584 | $params = $request->get_params(); |
| 585 | $message = isset( $params['message'] ) ? $params['message'] : ''; |
| 586 | if ( empty( $message ) ) { |
| 587 | $message = isset( $params['prompt'] ) ? $params['prompt'] : ''; |
| 588 | } |
| 589 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 590 | $options = $this->sanitize_rest_options( $options ); |
| 591 | $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api'; |
| 592 | if ( !empty( $scope ) ) { |
| 593 | $options['scope'] = $scope; |
| 594 | } |
| 595 | if ( empty( $message ) ) { |
| 596 | throw new Exception( 'The message is required.' ); |
| 597 | } |
| 598 | |
| 599 | if ( $this->debug ) { |
| 600 | $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 ); |
| 601 | $debug = sprintf( 'REST [SimpleJsonQuery]: %s, %s', $shortMessage, json_encode( $options ) ); |
| 602 | Meow_MWAI_Logging::log( $debug ); |
| 603 | } |
| 604 | |
| 605 | $reply = $this->simpleJsonQuery( $message, null, null, $options ); |
| 606 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 607 | } |
| 608 | catch ( Exception $e ) { |
| 609 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 610 | } |
| 611 | } |
| 612 | |
| 613 | public function rest_moderationCheck( $request ) { |
| 614 | try { |
| 615 | $params = $request->get_params(); |
| 616 | $text = isset( $params['text'] ) ? $params['text'] : ''; |
| 617 | |
| 618 | // Check for common parameter mistakes and provide helpful guidance |
| 619 | if ( empty( $text ) && isset( $params['message'] ) ) { |
| 620 | throw new Exception( 'Parameter "text" is required. Did you mean to use "text" instead of "message"?' ); |
| 621 | } |
| 622 | if ( empty( $text ) && isset( $params['content'] ) ) { |
| 623 | throw new Exception( 'Parameter "text" is required. Did you mean to use "text" instead of "content"?' ); |
| 624 | } |
| 625 | |
| 626 | if ( empty( $text ) ) { |
| 627 | throw new Exception( 'The "text" parameter is required for content moderation.' ); |
| 628 | } |
| 629 | |
| 630 | if ( $this->debug ) { |
| 631 | $shortText = Meow_MWAI_Logging::shorten( $text, 64 ); |
| 632 | $debug = sprintf( 'REST [ModerationCheck]: %s', $shortText ); |
| 633 | Meow_MWAI_Logging::log( $debug ); |
| 634 | } |
| 635 | |
| 636 | $reply = $this->moderationCheck( $text ); |
| 637 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 638 | } |
| 639 | catch ( Exception $e ) { |
| 640 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 641 | } |
| 642 | } |
| 643 | |
| 644 | public function rest_simpleTranscribeAudio( $request ) { |
| 645 | try { |
| 646 | $params = $request->get_params(); |
| 647 | $url = isset( $params['url'] ) ? $params['url'] : ''; |
| 648 | $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0; |
| 649 | |
| 650 | // Check for common parameter mistakes and provide helpful guidance |
| 651 | if ( empty( $url ) && empty( $mediaId ) ) { |
| 652 | if ( isset( $params['audioUrl'] ) ) { |
| 653 | throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "audioUrl"?' ); |
| 654 | } |
| 655 | if ( isset( $params['audio_url'] ) ) { |
| 656 | throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "audio_url"?' ); |
| 657 | } |
| 658 | if ( isset( $params['file'] ) ) { |
| 659 | throw new Exception( 'Use "url" for remote files or "mediaId" for uploaded files. Found "file" parameter instead.' ); |
| 660 | } |
| 661 | } |
| 662 | |
| 663 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 664 | $options = $this->sanitize_rest_options( $options ); |
| 665 | $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api'; |
| 666 | |
| 667 | if ( !empty( $scope ) ) { |
| 668 | $options['scope'] = $scope; |
| 669 | } |
| 670 | |
| 671 | // Get file path from mediaId if provided |
| 672 | $path = null; |
| 673 | if ( $mediaId > 0 ) { |
| 674 | $path = get_attached_file( $mediaId ); |
| 675 | if ( empty( $path ) ) { |
| 676 | throw new Exception( 'The media file cannot be found.' ); |
| 677 | } |
| 678 | } |
| 679 | |
| 680 | if ( empty( $url ) && empty( $path ) ) { |
| 681 | throw new Exception( 'Either a "url" parameter or a "mediaId" parameter is required for audio transcription.' ); |
| 682 | } |
| 683 | |
| 684 | if ( $this->debug ) { |
| 685 | $debug = sprintf( 'REST [SimpleTranscribeAudio]: url=%s, mediaId=%d, %s', |
| 686 | $url ? 'provided' : 'none', |
| 687 | $mediaId, |
| 688 | json_encode( $options ) |
| 689 | ); |
| 690 | Meow_MWAI_Logging::log( $debug ); |
| 691 | } |
| 692 | |
| 693 | $reply = $this->simpleTranscribeAudio( $url, $path, $options ); |
| 694 | return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 ); |
| 695 | } |
| 696 | catch ( Exception $e ) { |
| 697 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 698 | } |
| 699 | } |
| 700 | |
| 701 | public function rest_simpleFileUpload( $request ) { |
| 702 | try { |
| 703 | $params = $request->get_params(); |
| 704 | $files = $request->get_file_params(); |
| 705 | |
| 706 | // Check if file is provided |
| 707 | if ( empty( $files['file'] ) ) { |
| 708 | // Check for base64 encoded file data |
| 709 | $base64 = isset( $params['base64'] ) ? $params['base64'] : ''; |
| 710 | $filename = isset( $params['filename'] ) ? $params['filename'] : ''; |
| 711 | |
| 712 | if ( empty( $base64 ) ) { |
| 713 | throw new Exception( 'Either a file upload or base64 encoded data is required.' ); |
| 714 | } |
| 715 | |
| 716 | // Handle base64 upload |
| 717 | $options = isset( $params['options'] ) ? $params['options'] : []; |
| 718 | $purpose = isset( $params['purpose'] ) ? $params['purpose'] : 'files'; |
| 719 | $ttl = isset( $params['ttl'] ) ? intval( $params['ttl'] ) : 3600; |
| 720 | $target = isset( $params['target'] ) ? $params['target'] : null; |
| 721 | $metadata = isset( $params['metadata'] ) ? $params['metadata'] : []; |
| 722 | |
| 723 | if ( empty( $filename ) ) { |
| 724 | $filename = 'upload-' . time() . '.png'; // Default filename for base64 |
| 725 | } |
| 726 | |
| 727 | // Log the request if debug is enabled |
| 728 | if ( $this->debug ) { |
| 729 | $debug = sprintf( 'REST [SimpleFileUpload]: base64 upload, filename=%s, purpose=%s', |
| 730 | $filename, |
| 731 | $purpose |
| 732 | ); |
| 733 | Meow_MWAI_Logging::log( $debug ); |
| 734 | } |
| 735 | |
| 736 | $result = $this->simpleFileUpload( null, $base64, $filename, $purpose, $ttl, $target, $metadata ); |
| 737 | } |
| 738 | else { |
| 739 | // Handle regular file upload |
| 740 | $file = $files['file']; |
| 741 | $purpose = isset( $params['purpose'] ) ? $params['purpose'] : 'files'; |
| 742 | $ttl = isset( $params['ttl'] ) ? intval( $params['ttl'] ) : 3600; |
| 743 | $target = isset( $params['target'] ) ? $params['target'] : null; |
| 744 | $metadata = isset( $params['metadata'] ) ? $params['metadata'] : []; |
| 745 | |
| 746 | if ( $this->debug ) { |
| 747 | $debug = sprintf( 'REST [SimpleFileUpload]: file upload, name=%s, purpose=%s', |
| 748 | $file['name'], |
| 749 | $purpose |
| 750 | ); |
| 751 | Meow_MWAI_Logging::log( $debug ); |
| 752 | } |
| 753 | |
| 754 | $result = $this->simpleFileUpload( $file, null, null, $purpose, $ttl, $target, $metadata ); |
| 755 | } |
| 756 | |
| 757 | return new WP_REST_Response( [ 'success' => true, 'data' => $result ], 200 ); |
| 758 | } |
| 759 | catch ( Exception $e ) { |
| 760 | return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 ); |
| 761 | } |
| 762 | } |
| 763 | #endregion |
| 764 | |
| 765 | #region Simple API |
| 766 | /** |
| 767 | * Executes a vision query.` |
| 768 | * |
| 769 | * @param string $message The prompt for the AI. |
| 770 | * @param string $url The URL of the image to analyze. |
| 771 | * @param string|null $path The path to the image file. If provided, the image data will be read from this file. |
| 772 | * @param array $params Additional parameters for the AI query. |
| 773 | * |
| 774 | * @return string The result of the AI query. |
| 775 | */ |
| 776 | public function simpleVisionQuery( $message, $url, $path = null, $params = [] ) { |
| 777 | global $mwai_core; |
| 778 | $ai_vision_default_env = $this->core->get_option( 'ai_vision_default_env' ); |
| 779 | $ai_vision_default_model = $this->core->get_option( 'ai_vision_default_model' ); |
| 780 | if ( empty( $ai_vision_default_model ) ) { |
| 781 | $ai_vision_default_model = MWAI_FALLBACK_MODEL_VISION; |
| 782 | } |
| 783 | $query = new Meow_MWAI_Query_Text( $message ); |
| 784 | if ( !empty( $ai_vision_default_env ) ) { |
| 785 | $query->set_env_id( $ai_vision_default_env ); |
| 786 | } |
| 787 | if ( !empty( $ai_vision_default_model ) ) { |
| 788 | $query->set_model( $ai_vision_default_model ); |
| 789 | } |
| 790 | $query->inject_params( $params ); |
| 791 | if ( isset( $params['image_remote_upload'] ) ) { |
| 792 | $query->image_remote_upload = $params['image_remote_upload']; |
| 793 | } |
| 794 | if ( !empty( $url ) ) { |
| 795 | $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'vision' ) ); |
| 796 | } |
| 797 | else if ( !empty( $path ) ) { |
| 798 | $query->set_file( Meow_MWAI_Query_DroppedFile::from_path( $path, 'vision' ) ); |
| 799 | } |
| 800 | $reply = $mwai_core->run_query( $query ); |
| 801 | return $reply->result; |
| 802 | } |
| 803 | |
| 804 | /** |
| 805 | * Executes a chatbot query. |
| 806 | * It will use the discussion if chatId is provided in the parameters. |
| 807 | * |
| 808 | * @param string $botId The ID of the chatbot. |
| 809 | * @param string $message The prompt for the AI. |
| 810 | * @param array $params Additional parameters for the AI query. |
| 811 | * |
| 812 | * @return string The result of the AI query. |
| 813 | */ |
| 814 | public function simpleChatbotQuery( $botId, $message, $params = [], $onlyReply = true ) { |
| 815 | if ( !isset( $params['messages'] ) && isset( $params['chatId'] ) ) { |
| 816 | if ( $this->core->get_option( 'chatbot_discussions' ) && $this->discussions_module ) { |
| 817 | $discussion = $this->discussions_module->get_discussion( $botId, $params['chatId'] ); |
| 818 | if ( !empty( $discussion ) ) { |
| 819 | $params['messages'] = $discussion['messages']; |
| 820 | |
| 821 | // CRITICAL: Also pass the discussion metadata for Responses API support |
| 822 | // The chatbot module needs the previousResponseId from discussion's extra field |
| 823 | if ( !empty( $discussion['extra'] ) ) { |
| 824 | $extra = json_decode( $discussion['extra'], true ); |
| 825 | |
| 826 | // Check for both possible field names |
| 827 | $responseId = $extra['previousResponseId'] ?? $extra['responseId'] ?? null; |
| 828 | $responseDate = $extra['previousResponseDate'] ?? $extra['responseDate'] ?? null; |
| 829 | |
| 830 | if ( !empty( $responseId ) ) { |
| 831 | // Check if the response ID is still valid (not older than 30 days) |
| 832 | $responseDateTimestamp = !empty( $responseDate ) ? strtotime( $responseDate ) : 0; |
| 833 | $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 ); |
| 834 | |
| 835 | if ( $responseDateTimestamp > $thirtyDaysAgo ) { |
| 836 | // Pass the previousResponseId directly in params |
| 837 | // This will be picked up by inject_params in the query |
| 838 | $params['previousResponseId'] = $responseId; |
| 839 | } |
| 840 | } |
| 841 | } |
| 842 | } |
| 843 | } |
| 844 | else { |
| 845 | Meow_MWAI_Logging::log( 'The chatId was provided; but the discussions are not enabled.' ); |
| 846 | } |
| 847 | } |
| 848 | $fileId = isset( $params['fileId'] ) ? $params['fileId'] : null; |
| 849 | $data = $this->chatbot_module->chat_submit( $botId, $message, $fileId, $params ); |
| 850 | return $onlyReply ? $data['reply'] : $data; |
| 851 | } |
| 852 | |
| 853 | /** |
| 854 | * Executes a text query. |
| 855 | * |
| 856 | * @param string $message The prompt for the AI. |
| 857 | * @param array $params Additional parameters for the AI query. |
| 858 | * |
| 859 | * @return string The result of the AI query. |
| 860 | */ |
| 861 | public function simpleTextQuery( $message, $params = [] ) { |
| 862 | global $mwai_core; |
| 863 | $query = new Meow_MWAI_Query_Text( $message ); |
| 864 | $query->inject_params( $params ); |
| 865 | $reply = $mwai_core->run_query( $query ); |
| 866 | return $reply->result; |
| 867 | } |
| 868 | |
| 869 | public function simpleFastTextQuery( $message, $params = [] ) { |
| 870 | global $mwai_core; |
| 871 | $query = new Meow_MWAI_Query_Text( $message ); |
| 872 | |
| 873 | // Use the Default (Fast) model and environment |
| 874 | $fastDefaultModel = $mwai_core->get_option( 'ai_fast_default_model' ); |
| 875 | if ( !empty( $fastDefaultModel ) ) { |
| 876 | $query->set_model( $fastDefaultModel ); |
| 877 | } |
| 878 | |
| 879 | $fastDefaultEnv = $mwai_core->get_option( 'ai_fast_default_env' ); |
| 880 | if ( !empty( $fastDefaultEnv ) ) { |
| 881 | $query->set_env_id( $fastDefaultEnv ); |
| 882 | } |
| 883 | |
| 884 | // Inject any additional params (which may override the defaults) |
| 885 | $query->inject_params( $params ); |
| 886 | |
| 887 | try { |
| 888 | $reply = $mwai_core->run_query( $query ); |
| 889 | return $reply->result; |
| 890 | } |
| 891 | catch ( Exception $e ) { |
| 892 | // If Fast Model fails, try with default model |
| 893 | Meow_MWAI_Logging::warn( "Fast Model failed: " . $e->getMessage() . " - Falling back to default model." ); |
| 894 | |
| 895 | // Create a new query with default model/env |
| 896 | $fallbackQuery = new Meow_MWAI_Query_Text( $message ); |
| 897 | |
| 898 | $defaultModel = $mwai_core->get_option( 'ai_default_model' ); |
| 899 | if ( !empty( $defaultModel ) ) { |
| 900 | $fallbackQuery->set_model( $defaultModel ); |
| 901 | } |
| 902 | |
| 903 | $defaultEnv = $mwai_core->get_option( 'ai_default_env' ); |
| 904 | if ( !empty( $defaultEnv ) ) { |
| 905 | $fallbackQuery->set_env_id( $defaultEnv ); |
| 906 | } |
| 907 | |
| 908 | // Inject params again (except model/env which we just set) |
| 909 | $fallbackParams = $params; |
| 910 | unset( $fallbackParams['model'] ); |
| 911 | unset( $fallbackParams['envId'] ); |
| 912 | $fallbackQuery->inject_params( $fallbackParams ); |
| 913 | |
| 914 | $reply = $mwai_core->run_query( $fallbackQuery ); |
| 915 | return $reply->result; |
| 916 | } |
| 917 | } |
| 918 | |
| 919 | public function simpleImageQuery( $message, $params = [] ) { |
| 920 | global $mwai_core; |
| 921 | $query = new Meow_MWAI_Query_Image( $message ); |
| 922 | $query->inject_params( $params ); |
| 923 | $reply = $mwai_core->run_query( $query ); |
| 924 | return $reply->result; |
| 925 | } |
| 926 | |
| 927 | public function simpleImageEditQuery( $message, $mediaId, $params = [] ) { |
| 928 | global $mwai_core; |
| 929 | $query = new Meow_MWAI_Query_EditImage( $message ); |
| 930 | $query->inject_params( $params ); |
| 931 | $path = get_attached_file( $mediaId ); |
| 932 | if ( empty( $path ) ) { |
| 933 | throw new Exception( 'The media cannot be found.' ); |
| 934 | } |
| 935 | // TODO: Maybe 'vision' should be 'edit'. |
| 936 | $query->set_file( Meow_MWAI_Query_DroppedFile::from_path( $path, 'vision' ) ); |
| 937 | $reply = $mwai_core->run_query( $query ); |
| 938 | return $reply->result; |
| 939 | } |
| 940 | |
| 941 | /** |
| 942 | * Generates an image relevant to the text. |
| 943 | */ |
| 944 | public function imageQueryForMediaLibrary( $message, $params = [], $postId = null ) { |
| 945 | $query = new Meow_MWAI_Query_Image( $message ); |
| 946 | $query->inject_params( $params ); |
| 947 | $query->set_local_download( null ); |
| 948 | $reply = $this->core->run_query( $query ); |
| 949 | preg_match( '/\!\[Image\]\((.*?)\)/', $reply->result, $matches ); |
| 950 | $url = $matches[1] ?? $reply->result; |
| 951 | |
| 952 | // Check if the URL is already a WordPress attachment URL to avoid duplicates |
| 953 | $attachmentId = null; |
| 954 | $upload_dir = wp_upload_dir(); |
| 955 | if ( strpos( $url, $upload_dir['baseurl'] ) === 0 ) { |
| 956 | // This is already a local WordPress upload, try to find the attachment ID |
| 957 | // First try by GUID |
| 958 | global $wpdb; |
| 959 | $attachmentId = $wpdb->get_var( $wpdb->prepare( |
| 960 | "SELECT ID FROM {$wpdb->posts} WHERE guid = %s AND post_type = 'attachment'", |
| 961 | $url |
| 962 | ) ); |
| 963 | |
| 964 | // If not found by GUID, try by attachment URL (more reliable) |
| 965 | if ( empty( $attachmentId ) ) { |
| 966 | $attachmentId = attachment_url_to_postid( $url ); |
| 967 | } |
| 968 | } |
| 969 | |
| 970 | // If not found or not a local URL, add it to the media library |
| 971 | if ( empty( $attachmentId ) ) { |
| 972 | $attachmentId = $this->core->add_image_from_url( $url, null, null, null, null, null, $postId ); |
| 973 | if ( empty( $attachmentId ) ) { |
| 974 | throw new Exception( 'Could not add the image to the Media Library.' ); |
| 975 | } |
| 976 | } |
| 977 | |
| 978 | // TODO: We should create a nice title, caption, and alt. |
| 979 | $media = [ |
| 980 | 'id' => $attachmentId, |
| 981 | 'url' => wp_get_attachment_url( $attachmentId ), |
| 982 | 'title' => get_the_title( $attachmentId ), |
| 983 | 'caption' => wp_get_attachment_caption( $attachmentId ), |
| 984 | 'alt' => get_post_meta( $attachmentId, '_wp_attachment_image_alt', true ) |
| 985 | ]; |
| 986 | return $media; |
| 987 | } |
| 988 | |
| 989 | /** |
| 990 | * Executes a query that will have to return a JSON result. |
| 991 | * |
| 992 | * @param string $message The prompt for the AI. |
| 993 | * @param array $params Additional parameters for the AI query. |
| 994 | * |
| 995 | * @return array The result of the AI query. |
| 996 | */ |
| 997 | public function simpleJsonQuery( $message, $url = null, $path = null, $params = [] ) { |
| 998 | if ( !empty( $url ) || !empty( $path ) ) { |
| 999 | throw new Exception( 'The url and path are not supported yet by the simpleJsonQuery.' ); |
| 1000 | } |
| 1001 | global $mwai_core; |
| 1002 | $query = new Meow_MWAI_Query_Text( $message . "\nYour reply must be a formatted JSON." ); |
| 1003 | $query->inject_params( $params ); |
| 1004 | $query->set_response_format( 'json' ); |
| 1005 | $ai_json_default_env = $mwai_core->get_option( 'ai_json_default_env' ); |
| 1006 | $ai_json_default_model = $mwai_core->get_option( 'ai_json_default_model' ); |
| 1007 | if ( !empty( $ai_json_default_env ) ) { |
| 1008 | $query->set_env_id( $ai_json_default_env ); |
| 1009 | } |
| 1010 | if ( !empty( $ai_json_default_model ) ) { |
| 1011 | $query->set_model( $ai_json_default_model ); |
| 1012 | } |
| 1013 | else { |
| 1014 | $query->set_model( MWAI_FALLBACK_MODEL_JSON ); |
| 1015 | } |
| 1016 | $reply = $mwai_core->run_query( $query ); |
| 1017 | try { |
| 1018 | $json = json_decode( $reply->result, true ); |
| 1019 | return $json; |
| 1020 | } |
| 1021 | catch ( Exception $e ) { |
| 1022 | throw new Exception( 'The result is not a valid JSON.' ); |
| 1023 | } |
| 1024 | } |
| 1025 | |
| 1026 | /** |
| 1027 | * Uploads a file to the system. |
| 1028 | * |
| 1029 | * @param array|null $file The file array from $_FILES. |
| 1030 | * @param string|null $base64 Base64 encoded file data. |
| 1031 | * @param string|null $filename The filename for base64 uploads. |
| 1032 | * @param string $purpose The purpose of the file upload (e.g., 'files', 'vision', 'assistant'). |
| 1033 | * @param int $ttl Time to live in seconds. Default 3600 (1 hour). |
| 1034 | * @param string|null $target Target location: 'uploads' or 'library'. |
| 1035 | * @param array $metadata Additional metadata to store with the file. |
| 1036 | * |
| 1037 | * @return array Array with 'id' (refId) and 'url' of the uploaded file. |
| 1038 | */ |
| 1039 | public function simpleFileUpload( $file = null, $base64 = null, $filename = null, $purpose = 'files', $ttl = 3600, $target = null, $metadata = [] ) { |
| 1040 | global $mwai_core; |
| 1041 | |
| 1042 | if ( !$this->core->files ) { |
| 1043 | throw new Exception( 'Files module is not available.' ); |
| 1044 | } |
| 1045 | |
| 1046 | // Determine target from settings if not provided |
| 1047 | if ( empty( $target ) ) { |
| 1048 | $target = $this->core->get_option( 'image_local_upload', 'uploads' ); |
| 1049 | } |
| 1050 | |
| 1051 | try { |
| 1052 | if ( !empty( $base64 ) ) { |
| 1053 | // Handle base64 upload |
| 1054 | if ( empty( $filename ) ) { |
| 1055 | $filename = 'upload-' . time() . '.dat'; |
| 1056 | } |
| 1057 | |
| 1058 | // Validate filename extension for base64 uploads |
| 1059 | $validate = wp_check_filetype( $filename ); |
| 1060 | if ( $validate['type'] == false ) { |
| 1061 | throw new Exception( 'File type is not allowed.' ); |
| 1062 | } |
| 1063 | |
| 1064 | // For base64 uploads, we need to decode and create a temp file first |
| 1065 | $binary = base64_decode( $base64 ); |
| 1066 | if ( !$binary ) { |
| 1067 | throw new Exception( 'Invalid base64 data.' ); |
| 1068 | } |
| 1069 | |
| 1070 | // Create a temporary file |
| 1071 | $tmp_path = wp_tempnam( 'mwai-upload' ); |
| 1072 | file_put_contents( $tmp_path, $binary ); |
| 1073 | |
| 1074 | // Use the regular upload method |
| 1075 | $refId = $this->core->files->upload_file( |
| 1076 | $tmp_path, |
| 1077 | $filename, |
| 1078 | $purpose, |
| 1079 | $metadata, |
| 1080 | null, // envId |
| 1081 | $target, |
| 1082 | $ttl |
| 1083 | ); |
| 1084 | |
| 1085 | // Clean up temp file if it was uploaded to library |
| 1086 | if ( $target === 'library' && file_exists( $tmp_path ) ) { |
| 1087 | @unlink( $tmp_path ); |
| 1088 | } |
| 1089 | |
| 1090 | $url = $this->core->files->get_url( $refId ); |
| 1091 | |
| 1092 | return [ |
| 1093 | 'id' => $refId, |
| 1094 | 'url' => $url |
| 1095 | ]; |
| 1096 | } |
| 1097 | else if ( !empty( $file ) && is_array( $file ) ) { |
| 1098 | // Handle regular file upload |
| 1099 | if ( !empty( $file['error'] ) ) { |
| 1100 | throw new Exception( 'File upload error: ' . $file['error'] ); |
| 1101 | } |
| 1102 | |
| 1103 | $refId = $this->core->files->upload_file( |
| 1104 | $file['tmp_name'], |
| 1105 | $file['name'], |
| 1106 | $purpose, |
| 1107 | $metadata, |
| 1108 | null, // envId |
| 1109 | $target, |
| 1110 | $ttl |
| 1111 | ); |
| 1112 | |
| 1113 | $url = $this->core->files->get_url( $refId ); |
| 1114 | |
| 1115 | return [ |
| 1116 | 'id' => $refId, |
| 1117 | 'url' => $url |
| 1118 | ]; |
| 1119 | } |
| 1120 | else { |
| 1121 | throw new Exception( 'Either a file or base64 data must be provided.' ); |
| 1122 | } |
| 1123 | } |
| 1124 | catch ( Exception $e ) { |
| 1125 | throw new Exception( 'File upload failed: ' . $e->getMessage() ); |
| 1126 | } |
| 1127 | } |
| 1128 | |
| 1129 | /** |
| 1130 | * Executes an audio transcription query. |
| 1131 | * |
| 1132 | * @param string $url The URL of the audio file to transcribe. |
| 1133 | * @param string|null $path The path to the audio file. If provided, the audio data will be read from this file. |
| 1134 | * @param array $params Additional parameters for the transcription query. |
| 1135 | * |
| 1136 | * @return string The transcribed text. |
| 1137 | */ |
| 1138 | public function simpleTranscribeAudio( $url = null, $path = null, $params = [] ) { |
| 1139 | global $mwai_core; |
| 1140 | $ai_audio_default_env = $this->core->get_option( 'ai_audio_default_env' ); |
| 1141 | $ai_audio_default_model = $this->core->get_option( 'ai_audio_default_model' ); |
| 1142 | |
| 1143 | if ( empty( $ai_audio_default_model ) ) { |
| 1144 | $ai_audio_default_model = 'whisper-1'; // Default transcription model |
| 1145 | } |
| 1146 | |
| 1147 | $query = new Meow_MWAI_Query_Transcribe(); |
| 1148 | |
| 1149 | if ( !empty( $ai_audio_default_env ) ) { |
| 1150 | $query->set_env_id( $ai_audio_default_env ); |
| 1151 | } |
| 1152 | if ( !empty( $ai_audio_default_model ) ) { |
| 1153 | $query->set_model( $ai_audio_default_model ); |
| 1154 | } |
| 1155 | |
| 1156 | $query->inject_params( $params ); |
| 1157 | |
| 1158 | if ( !empty( $url ) ) { |
| 1159 | // Use 'files' as the purpose for audio files |
| 1160 | $query->set_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'files' ) ); |
| 1161 | } |
| 1162 | else if ( !empty( $path ) ) { |
| 1163 | // Use 'files' as the purpose for audio files |
| 1164 | $query->set_file( Meow_MWAI_Query_DroppedFile::from_path( $path, 'files' ) ); |
| 1165 | } |
| 1166 | else { |
| 1167 | throw new Exception( 'Either a URL or a path must be provided for the audio file.' ); |
| 1168 | } |
| 1169 | |
| 1170 | $reply = $mwai_core->run_query( $query ); |
| 1171 | return $reply->result; |
| 1172 | } |
| 1173 | #endregion |
| 1174 | |
| 1175 | #region Standard API |
| 1176 | /** |
| 1177 | * Checks if a text is safe or not. |
| 1178 | * |
| 1179 | * @param string $text The text to check. |
| 1180 | * |
| 1181 | * @return bool True if the text is safe, false otherwise. |
| 1182 | */ |
| 1183 | public function moderationCheck( $text ) { |
| 1184 | global $mwai_core; |
| 1185 | $openai = Meow_MWAI_Engines_Factory::get_openai( $mwai_core ); |
| 1186 | $res = $openai->moderate( $text ); |
| 1187 | if ( !empty( $res ) && !empty( $res['results'] ) ) { |
| 1188 | return (bool) $res['results'][0]['flagged']; |
| 1189 | } |
| 1190 | } |
| 1191 | #endregion |
| 1192 | |
| 1193 | #region Standard API (No REST API) |
| 1194 | |
| 1195 | /** |
| 1196 | * Checks the status of the AI environments. |
| 1197 | * |
| 1198 | * @return array The types of environments that are available. |
| 1199 | */ |
| 1200 | public function checkStatus() { |
| 1201 | $env_types = []; |
| 1202 | $ai_envs = $this->core->get_option( 'ai_envs' ); |
| 1203 | if ( empty( $ai_envs ) ) { |
| 1204 | throw new Exception( 'There are no AI environments yet.' ); |
| 1205 | } |
| 1206 | foreach ( $ai_envs as $env ) { |
| 1207 | if ( !empty( $env['apikey'] ) ) { |
| 1208 | if ( !in_array( $env['type'], $env_types ) ) { |
| 1209 | $env_types[] = $env['type']; |
| 1210 | } |
| 1211 | } |
| 1212 | } |
| 1213 | if ( empty( $env_types ) ) { |
| 1214 | throw new Exception( 'There are no AI environments with an API key yet.' ); |
| 1215 | } |
| 1216 | return $env_types; |
| 1217 | } |
| 1218 | |
| 1219 | /** |
| 1220 | * Get function name by ID |
| 1221 | */ |
| 1222 | private function get_function_name_by_id( $funcId ) { |
| 1223 | // Get function from registry using the static method |
| 1224 | $function = MeowPro_MWAI_FunctionAware::get_function( 'code-engine', $funcId ); |
| 1225 | if ( $function && isset( $function->name ) ) { |
| 1226 | return $function->name; |
| 1227 | } |
| 1228 | |
| 1229 | // If not found, try snippet-vault type as well |
| 1230 | $function = MeowPro_MWAI_FunctionAware::get_function( 'snippet-vault', $funcId ); |
| 1231 | if ( $function && isset( $function->name ) ) { |
| 1232 | return $function->name; |
| 1233 | } |
| 1234 | |
| 1235 | return null; |
| 1236 | } |
| 1237 | #endregion |
| 1238 | } |
| 1239 |