PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.7.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.7.0
3.7.0 3.6.9 3.6.8 3.6.7 3.6.6 3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / api.php
ai-engine / classes Last commit date
data 1 year ago engines 1 week ago exceptions 1 week ago modules 1 week ago query 1 week ago services 1 week ago admin.php 1 week ago api.php 1 week ago core.php 1 week ago discussion.php 1 year ago event.php 1 year ago init.php 1 week ago logging.php 1 year ago reply.php 1 week ago rest.php 1 week ago
api.php
1307 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 return Meow_MWAI_Core::sanitize_rest_params( $options );
26 }
27
28 public function rest_api_init() {
29 $public_api = $this->core->get_option( 'public_api' );
30 if ( !$public_api ) {
31 return;
32 }
33 $this->bearer_token = $this->core->get_option( 'public_api_bearer_token' );
34 if ( !empty( $this->bearer_token ) ) {
35 add_filter( 'mwai_allow_public_api', [ $this, 'auth_via_bearer_token' ], 10, 3 );
36 }
37
38 register_rest_route( 'mwai/v1', '/simpleAuthCheck', [
39 'methods' => 'GET',
40 'callback' => [ $this, 'rest_simpleAuthCheck' ],
41 'permission_callback' => function ( $request ) {
42 return $this->core->can_access_public_api( 'simpleAuthCheck', $request );
43 },
44 ] );
45 register_rest_route( 'mwai/v1', '/simpleTextQuery', [
46 'methods' => 'POST',
47 'callback' => [ $this, 'rest_simpleTextQuery' ],
48 'permission_callback' => function ( $request ) {
49 return $this->core->can_access_public_api( 'simpleTextQuery', $request );
50 },
51 ] );
52 register_rest_route( 'mwai/v1', '/simpleFastTextQuery', [
53 'methods' => 'POST',
54 'callback' => [ $this, 'rest_simpleFastTextQuery' ],
55 'permission_callback' => function ( $request ) {
56 return $this->core->can_access_public_api( 'simpleFastTextQuery', $request );
57 },
58 ] );
59 register_rest_route( 'mwai/v1', '/simpleImageQuery', [
60 'methods' => 'POST',
61 'callback' => [ $this, 'rest_simpleImageQuery' ],
62 'permission_callback' => function ( $request ) {
63 return $this->core->can_access_public_api( 'simpleImageQuery', $request );
64 },
65 ] );
66 register_rest_route( 'mwai/v1', '/simpleImageEditQuery', [
67 'methods' => 'POST',
68 'callback' => [ $this, 'rest_simpleImageEditQuery' ],
69 'permission_callback' => function ( $request ) {
70 return $this->core->can_access_public_api( 'simpleImageEditQuery', $request );
71 },
72 ] );
73 register_rest_route( 'mwai/v1', '/simpleVisionQuery', [
74 'methods' => 'POST',
75 'callback' => [ $this, 'rest_simpleVisionQuery' ],
76 'permission_callback' => function ( $request ) {
77 return $this->core->can_access_public_api( 'simpleVisionQuery', $request );
78 },
79 ] );
80 register_rest_route( 'mwai/v1', '/simpleJsonQuery', [
81 'methods' => 'POST',
82 'callback' => [ $this, 'rest_simpleJsonQuery' ],
83 'permission_callback' => function ( $request ) {
84 return $this->core->can_access_public_api( 'simpleJsonQuery', $request );
85 },
86 ] );
87 register_rest_route( 'mwai/v1', '/moderationCheck', [
88 'methods' => 'POST',
89 'callback' => [ $this, 'rest_moderationCheck' ],
90 'permission_callback' => function ( $request ) {
91 return $this->core->can_access_public_api( 'moderationCheck', $request );
92 },
93 ] );
94 register_rest_route( 'mwai/v1', '/simpleTranscribeAudio', [
95 'methods' => 'POST',
96 'callback' => [ $this, 'rest_simpleTranscribeAudio' ],
97 'permission_callback' => function ( $request ) {
98 return $this->core->can_access_public_api( 'simpleTranscribeAudio', $request );
99 },
100 ] );
101 register_rest_route( 'mwai/v1', '/simpleFileUpload', [
102 'methods' => 'POST',
103 'callback' => [ $this, 'rest_simpleFileUpload' ],
104 'permission_callback' => function ( $request ) {
105 return $this->core->can_access_public_api( 'simpleFileUpload', $request );
106 },
107 ] );
108
109 if ( $this->chatbot_module ) {
110 register_rest_route( 'mwai/v1', '/simpleChatbotQuery', [
111 'methods' => 'POST',
112 'callback' => [ $this, 'rest_simpleChatbotQuery' ],
113 'permission_callback' => function ( $request ) {
114 return $this->core->can_access_public_api( 'simpleChatbotQuery', $request );
115 },
116 ] );
117
118 register_rest_route( 'mwai/v1', '/listChatbots', [
119 'methods' => 'GET',
120 'callback' => [ $this, 'rest_listChatbots' ],
121 'permission_callback' => function ( $request ) {
122 return $this->core->can_access_public_api( 'listChatbots', $request );
123 },
124 ] );
125 }
126 }
127
128 public function rest_simpleAuthCheck( $request ) {
129 try {
130 $params = $request->get_params();
131 $current_user = wp_get_current_user();
132 $current_email = $current_user->user_email;
133 return new WP_REST_Response( [ 'success' => true, 'data' => [
134 'type' => 'email',
135 'value' => $current_email
136 ] ], 200 );
137 }
138 catch ( Exception $e ) {
139 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
140 }
141 }
142
143 public function auth_via_bearer_token( $allow, $feature, $extra ) {
144 if ( !empty( $extra ) && !empty( $extra->get_header( 'Authorization' ) ) ) {
145 $token = $extra->get_header( 'Authorization' );
146 $token = str_replace( 'Bearer ', '', $token );
147 if ( $token === $this->bearer_token ) {
148 // We set the current user to the first admin.
149 $admin = $this->core->get_admin_user();
150 wp_set_current_user( $admin->ID, $admin->user_login );
151 return true;
152 }
153 }
154 return $allow;
155 }
156
157 public function rest_simpleChatbotQuery( $request ) {
158 try {
159 $params = $request->get_params();
160 $botId = isset( $params['botId'] ) ? $params['botId'] : '';
161 $message = isset( $params['message'] ) ? $params['message'] : '';
162 if ( empty( $message ) ) {
163 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
164 }
165 $chatId = isset( $params['chatId'] ) ? $params['chatId'] : null;
166 $fileId = isset( $params['fileId'] ) ? $params['fileId'] : null;
167 $fileIds = isset( $params['fileIds'] ) ? $params['fileIds'] : null;
168 $queryParams = [];
169 if ( !empty( $chatId ) ) {
170 $queryParams['chatId'] = $chatId;
171 }
172 if ( !empty( $fileId ) ) {
173 $queryParams['fileId'] = $fileId;
174 }
175 if ( !empty( $fileIds ) && is_array( $fileIds ) ) {
176 $queryParams['fileIds'] = $fileIds;
177 }
178 if ( empty( $botId ) || empty( $message ) ) {
179 throw new Exception( __( 'The botId and message are required.', 'ai-engine' ) );
180 }
181
182 if ( $this->debug ) {
183 $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 );
184 $debug = sprintf( 'REST [SimpleChatbotQuery]: %s, %s', $shortMessage, json_encode( $queryParams ) );
185 Meow_MWAI_Logging::log( $debug );
186 }
187
188 $reply = $this->simpleChatbotQuery( $botId, $message, $queryParams, false );
189 return new WP_REST_Response( [
190 'success' => true,
191 'data' => $reply['reply'],
192 'extra' => [
193 'actions' => $reply['actions'],
194 'chatId' => $reply['chatId']
195 ]
196 ], 200 );
197 }
198 catch ( Exception $e ) {
199 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
200 }
201 }
202
203 public function rest_listChatbots( $request ) {
204 try {
205 // Get all chatbots
206 $chatbots = get_option( 'mwai_chatbots', [] );
207 $environments = $this->core->get_option( 'ai_envs' );
208 $mcp_envs = $this->core->get_option( 'mcp_envs', [] );
209
210 // Get all models from all environments
211 $all_models = [];
212 foreach ( $environments as $env ) {
213 try {
214 $engine = Meow_MWAI_Engines_Factory::get( $this->core, $env['id'] );
215 $env_models = $engine->retrieve_models();
216 foreach ( $env_models as $model ) {
217 $all_models[$model['model']] = $model;
218 }
219 }
220 catch ( Exception $e ) {
221 // Skip environments that fail
222 }
223 }
224
225 // Debug: Log model info for gpt-4.1-mini
226 if ( $this->debug && isset( $all_models['gpt-4.1-mini'] ) ) {
227 error_log( '[AI Engine API] Model info for gpt-4.1-mini: ' . json_encode( $all_models['gpt-4.1-mini'] ) );
228 }
229
230 // Get registered functions
231 $functions = apply_filters( 'mwai_functions_list', [] );
232 $function_names = [];
233 foreach ( $functions as $function ) {
234 $function_names[] = $function->name ?? 'unknown';
235 }
236
237 $result = [];
238
239 foreach ( $chatbots as $chatbotId => $chatbot ) {
240 // Debug log the chatbot structure
241 if ( $this->debug && ( $chatbot['name'] ?? '' ) === 'Jordy' ) {
242 error_log( '[AI Engine API] Jordy chatbot structure: ' . json_encode( $chatbot ) );
243 }
244
245 // Basic info
246 $info = [
247 'id' => $chatbot['botId'] ?? $chatbotId, // Use botId if available, fallback to array index
248 'name' => $chatbot['name'] ?? 'Unnamed',
249 'type' => 'chat', // Default type
250 'model' => null,
251 'model_name' => null,
252 'environment' => null,
253 'environment_name' => null,
254 'functions' => [],
255 'tools' => [], // Add tools array
256 'mcp_servers' => []
257 ];
258
259 // Determine chatbot type
260 if ( !empty( $chatbot['type'] ) ) {
261 $info['type'] = $chatbot['type'];
262 }
263 else {
264 // Try to infer type from model or other properties
265 if ( !empty( $chatbot['model'] ) ) {
266 if ( strpos( $chatbot['model'], 'realtime' ) !== false ) {
267 $info['type'] = 'realtime';
268 }
269 else if ( strpos( $chatbot['model'], 'image' ) !== false ) {
270 $info['type'] = 'images';
271 }
272 else if ( !empty( $chatbot['assistantId'] ) ) {
273 $info['type'] = 'assistant';
274 }
275 }
276 }
277
278 // Get model info
279 if ( !empty( $chatbot['model'] ) ) {
280 $info['model'] = $chatbot['model'];
281 // Find model name
282 if ( isset( $all_models[$chatbot['model']] ) ) {
283 $info['model_name'] = $all_models[$chatbot['model']]['name'] ?? $chatbot['model'];
284 }
285 else {
286 $info['model_name'] = $chatbot['model'];
287 }
288 }
289
290 // Get environment info
291 if ( !empty( $chatbot['envId'] ) ) {
292 $info['environment'] = $chatbot['envId'];
293 // Find environment name
294 foreach ( $environments as $env ) {
295 if ( $env['id'] === $chatbot['envId'] ) {
296 $info['environment_name'] = $env['name'] ?? $chatbot['envId'];
297 break;
298 }
299 }
300 }
301
302 // Check if it uses functions - get specific function names if available
303 if ( !empty( $chatbot['functions'] ) ) {
304 if ( is_array( $chatbot['functions'] ) ) {
305 // Functions are stored as array of objects with id and type
306 $chatbot_functions = [];
307 foreach ( $chatbot['functions'] as $func ) {
308 if ( isset( $func['id'] ) ) {
309 // Try to find function name by ID
310 $func_name = $this->get_function_name_by_id( $func['id'] );
311 if ( $func_name ) {
312 $chatbot_functions[] = $func_name;
313 }
314 else {
315 // Fallback: include the ID if name not found
316 $chatbot_functions[] = 'function_' . $func['id'];
317 }
318 }
319 }
320 $info['functions'] = $chatbot_functions;
321 }
322 else if ( $chatbot['functions'] === true ) {
323 // If functions is just true, it uses all registered functions
324 $info['functions'] = $function_names;
325 }
326 }
327
328 // Check for tools (Web Search, Image Generation)
329 if ( !empty( $chatbot['tools'] ) && is_array( $chatbot['tools'] ) ) {
330 // Filter tools based on model capabilities
331 $supported_tools = [];
332 if ( !empty( $chatbot['model'] ) ) {
333 // Try exact match first
334 $model_key = $chatbot['model'];
335 $model_info = $all_models[$model_key] ?? null;
336
337 // If not found and it's an OpenRouter model, try without prefix
338 if ( !$model_info && strpos( $model_key, '/' ) !== false ) {
339 $model_key = substr( $model_key, strpos( $model_key, '/' ) + 1 );
340 $model_info = $all_models[$model_key] ?? null;
341 }
342
343 if ( $model_info ) {
344 $model_tools = $model_info['tools'] ?? [];
345
346 // Only include tools that are both configured AND supported by the model
347 foreach ( $chatbot['tools'] as $tool ) {
348 if ( in_array( $tool, $model_tools ) ) {
349 $supported_tools[] = $tool;
350 }
351 }
352 }
353 else {
354 // If model not found in our list, keep all configured tools
355 // This allows custom models to use tools
356 $supported_tools = $chatbot['tools'];
357 }
358 }
359 $info['tools'] = $supported_tools;
360 }
361
362 // Check for MCP servers
363 if ( !empty( $chatbot['mcp_servers'] ) && is_array( $chatbot['mcp_servers'] ) ) {
364 foreach ( $chatbot['mcp_servers'] as $mcpServer ) {
365 if ( !empty( $mcpServer['id'] ) ) {
366 // Find MCP server name
367 foreach ( $mcp_envs as $mcp ) {
368 if ( $mcp['id'] === $mcpServer['id'] ) {
369 $info['mcp_servers'][] = [
370 'id' => $mcp['id'],
371 'name' => $mcp['name'] ?? 'Unnamed MCP Server'
372 ];
373 break;
374 }
375 }
376 }
377 }
378 }
379
380 $result[] = $info;
381 }
382
383 return new WP_REST_Response( [
384 'success' => true,
385 'data' => $result
386 ], 200 );
387 }
388 catch ( Exception $e ) {
389 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
390 }
391 }
392
393 public function rest_simpleTextQuery( $request ) {
394 try {
395 $params = $request->get_params();
396 $message = isset( $params['message'] ) ? $params['message'] : '';
397 if ( empty( $message ) ) {
398 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
399 }
400 $options = isset( $params['options'] ) ? $params['options'] : [];
401 $options = $this->sanitize_rest_options( $options );
402 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
403 if ( !empty( $scope ) ) {
404 $options['scope'] = $scope;
405 }
406 if ( empty( $message ) ) {
407 throw new Exception( __( 'The message is required.', 'ai-engine' ) );
408 }
409
410 if ( $this->debug ) {
411 $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 );
412 $debug = sprintf( 'REST [SimpleTextQuery]: %s, %s', $shortMessage, json_encode( $options ) );
413 Meow_MWAI_Logging::log( $debug );
414 }
415
416 $reply = $this->simpleTextQuery( $message, $options );
417 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
418 }
419 catch ( Exception $e ) {
420 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
421 }
422 }
423
424 public function rest_simpleFastTextQuery( $request ) {
425 try {
426 $params = $request->get_params();
427 $message = isset( $params['message'] ) ? $params['message'] : '';
428 if ( empty( $message ) ) {
429 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
430 }
431 $options = isset( $params['options'] ) ? $params['options'] : [];
432 $options = $this->sanitize_rest_options( $options );
433 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
434 if ( !empty( $scope ) ) {
435 $options['scope'] = $scope;
436 }
437 if ( empty( $message ) ) {
438 throw new Exception( __( 'The message is required.', 'ai-engine' ) );
439 }
440
441 if ( $this->debug ) {
442 $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 );
443 $debug = sprintf( 'REST [SimpleFastTextQuery]: %s, %s', $shortMessage, json_encode( $options ) );
444 Meow_MWAI_Logging::log( $debug );
445 }
446
447 $reply = $this->simpleFastTextQuery( $message, $options );
448 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
449 }
450 catch ( Exception $e ) {
451 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
452 }
453 }
454
455 public function rest_simpleImageQuery( $request ) {
456 try {
457 $params = $request->get_params();
458 $message = isset( $params['message'] ) ? $params['message'] : '';
459 if ( empty( $message ) ) {
460 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
461 }
462 $options = isset( $params['options'] ) ? $params['options'] : [];
463 $resolution = isset( $params['resolution'] ) ? $params['resolution'] : '';
464 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
465 if ( !empty( $scope ) ) {
466 $options['scope'] = $scope;
467 }
468 if ( empty( $message ) ) {
469 throw new Exception( __( 'The message is required.', 'ai-engine' ) );
470 }
471 if ( !empty( $resolution ) ) {
472 $options['resolution'] = $resolution;
473 }
474
475 if ( $this->debug ) {
476 $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 );
477 $debug = sprintf( 'REST [SimpleImageQuery]: %s, %s', $shortMessage, json_encode( $options ) );
478 Meow_MWAI_Logging::log( $debug );
479 }
480
481 $reply = $this->simpleImageQuery( $message, $options );
482 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
483 }
484 catch ( Exception $e ) {
485 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
486 }
487 }
488
489 public function rest_simpleImageEditQuery( $request ) {
490 try {
491 $params = $request->get_params();
492 $message = isset( $params['message'] ) ? $params['message'] : '';
493 if ( empty( $message ) ) {
494 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
495 }
496 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
497 $options = isset( $params['options'] ) ? $params['options'] : [];
498 $resolution = isset( $params['resolution'] ) ? $params['resolution'] : '';
499 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
500 if ( !empty( $scope ) ) {
501 $options['scope'] = $scope;
502 }
503 if ( empty( $message ) ) {
504 throw new Exception( __( 'The message is required.', 'ai-engine' ) );
505 }
506 if ( empty( $mediaId ) ) {
507 throw new Exception( 'The mediaId is required.' );
508 }
509 if ( !empty( $resolution ) ) {
510 $options['resolution'] = $resolution;
511 }
512
513 if ( $this->debug ) {
514 $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 );
515 $debug = sprintf( 'REST [SimpleImageEditQuery]: %s, %s', $shortMessage, json_encode( $options ) );
516 Meow_MWAI_Logging::log( $debug );
517 }
518
519 $reply = $this->simpleImageEditQuery( $message, $mediaId, $options );
520 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
521 }
522 catch ( Exception $e ) {
523 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
524 }
525 }
526
527 public function rest_simpleVisionQuery( $request ) {
528 try {
529 $params = $request->get_params();
530 $message = isset( $params['message'] ) ? $params['message'] : '';
531 if ( empty( $message ) ) {
532 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
533 }
534 $url = isset( $params['url'] ) ? $params['url'] : '';
535
536 // Check for common parameter mistakes and provide helpful guidance
537 if ( empty( $url ) && isset( $params['imageUrl'] ) ) {
538 throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "imageUrl"?' );
539 }
540 if ( empty( $url ) && isset( $params['image_url'] ) ) {
541 throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "image_url"?' );
542 }
543
544 $options = isset( $params['options'] ) ? $params['options'] : [];
545 $options = $this->sanitize_rest_options( $options );
546 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
547 if ( !empty( $scope ) ) {
548 $options['scope'] = $scope;
549 }
550 if ( empty( $message ) ) {
551 throw new Exception( __( 'The message is required.', 'ai-engine' ) );
552 }
553 if ( empty( $url ) ) {
554 throw new Exception( 'The "url" parameter is required for image analysis.' );
555 }
556
557 if ( $this->debug ) {
558 $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 );
559 $debug = sprintf( 'REST [SimpleVisionQuery]: %s, %s', $shortMessage, json_encode( $options ) );
560 Meow_MWAI_Logging::log( $debug );
561 }
562
563 $reply = $this->simpleVisionQuery( $message, $url, null, $options );
564 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
565 }
566 catch ( Exception $e ) {
567 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
568 }
569 }
570
571 public function rest_simpleJsonQuery( $request ) {
572 try {
573 $params = $request->get_params();
574 $message = isset( $params['message'] ) ? $params['message'] : '';
575 if ( empty( $message ) ) {
576 $message = isset( $params['prompt'] ) ? $params['prompt'] : '';
577 }
578 $options = isset( $params['options'] ) ? $params['options'] : [];
579 $options = $this->sanitize_rest_options( $options );
580 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
581 if ( !empty( $scope ) ) {
582 $options['scope'] = $scope;
583 }
584 if ( empty( $message ) ) {
585 throw new Exception( __( 'The message is required.', 'ai-engine' ) );
586 }
587
588 if ( $this->debug ) {
589 $shortMessage = Meow_MWAI_Logging::shorten( $message, 64 );
590 $debug = sprintf( 'REST [SimpleJsonQuery]: %s, %s', $shortMessage, json_encode( $options ) );
591 Meow_MWAI_Logging::log( $debug );
592 }
593
594 $reply = $this->simpleJsonQuery( $message, null, null, $options );
595 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
596 }
597 catch ( Exception $e ) {
598 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
599 }
600 }
601
602 public function rest_moderationCheck( $request ) {
603 try {
604 $params = $request->get_params();
605 $text = isset( $params['text'] ) ? $params['text'] : '';
606
607 // Check for common parameter mistakes and provide helpful guidance
608 if ( empty( $text ) && isset( $params['message'] ) ) {
609 throw new Exception( 'Parameter "text" is required. Did you mean to use "text" instead of "message"?' );
610 }
611 if ( empty( $text ) && isset( $params['content'] ) ) {
612 throw new Exception( 'Parameter "text" is required. Did you mean to use "text" instead of "content"?' );
613 }
614
615 if ( empty( $text ) ) {
616 throw new Exception( 'The "text" parameter is required for content moderation.' );
617 }
618
619 if ( $this->debug ) {
620 $shortText = Meow_MWAI_Logging::shorten( $text, 64 );
621 $debug = sprintf( 'REST [ModerationCheck]: %s', $shortText );
622 Meow_MWAI_Logging::log( $debug );
623 }
624
625 $reply = $this->moderationCheck( $text );
626 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
627 }
628 catch ( Exception $e ) {
629 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
630 }
631 }
632
633 public function rest_simpleTranscribeAudio( $request ) {
634 try {
635 $params = $request->get_params();
636 $url = isset( $params['url'] ) ? $params['url'] : '';
637 $mediaId = isset( $params['mediaId'] ) ? intval( $params['mediaId'] ) : 0;
638
639 // Check for common parameter mistakes and provide helpful guidance
640 if ( empty( $url ) && empty( $mediaId ) ) {
641 if ( isset( $params['audioUrl'] ) ) {
642 throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "audioUrl"?' );
643 }
644 if ( isset( $params['audio_url'] ) ) {
645 throw new Exception( 'Parameter "url" is required. Did you mean to use "url" instead of "audio_url"?' );
646 }
647 if ( isset( $params['file'] ) ) {
648 throw new Exception( 'Use "url" for remote files or "mediaId" for uploaded files. Found "file" parameter instead.' );
649 }
650 }
651
652 $options = isset( $params['options'] ) ? $params['options'] : [];
653 $options = $this->sanitize_rest_options( $options );
654 $scope = isset( $params['scope'] ) ? $params['scope'] : 'public-api';
655
656 if ( !empty( $scope ) ) {
657 $options['scope'] = $scope;
658 }
659
660 // Get file path from mediaId if provided
661 $path = null;
662 if ( $mediaId > 0 ) {
663 $path = get_attached_file( $mediaId );
664 if ( empty( $path ) ) {
665 throw new Exception( 'The media file cannot be found.' );
666 }
667 }
668
669 if ( empty( $url ) && empty( $path ) ) {
670 throw new Exception( 'Either a "url" parameter or a "mediaId" parameter is required for audio transcription.' );
671 }
672
673 if ( $this->debug ) {
674 $debug = sprintf(
675 'REST [SimpleTranscribeAudio]: url=%s, mediaId=%d, %s',
676 $url ? 'provided' : 'none',
677 $mediaId,
678 json_encode( $options )
679 );
680 Meow_MWAI_Logging::log( $debug );
681 }
682
683 $reply = $this->simpleTranscribeAudio( $url, $path, $options );
684 return new WP_REST_Response( [ 'success' => true, 'data' => $reply ], 200 );
685 }
686 catch ( Exception $e ) {
687 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
688 }
689 }
690
691 public function rest_simpleFileUpload( $request ) {
692 try {
693 $params = $request->get_params();
694 $files = $request->get_file_params();
695
696 // Check if file is provided
697 if ( empty( $files['file'] ) ) {
698 // Check for base64 encoded file data
699 $base64 = isset( $params['base64'] ) ? $params['base64'] : '';
700 $filename = isset( $params['filename'] ) ? $params['filename'] : '';
701
702 if ( empty( $base64 ) ) {
703 throw new Exception( 'Either a file upload or base64 encoded data is required.' );
704 }
705
706 // Handle base64 upload
707 $options = isset( $params['options'] ) ? $params['options'] : [];
708 $purpose = isset( $params['purpose'] ) ? $params['purpose'] : 'analysis';
709 $ttl = isset( $params['ttl'] ) ? intval( $params['ttl'] ) : 3600;
710 $target = isset( $params['target'] ) ? $params['target'] : null;
711 $metadata = isset( $params['metadata'] ) ? $params['metadata'] : [];
712
713 if ( empty( $filename ) ) {
714 $filename = 'upload-' . time() . '.png'; // Default filename for base64
715 }
716
717 // Log the request if debug is enabled
718 if ( $this->debug ) {
719 $debug = sprintf(
720 'REST [SimpleFileUpload]: base64 upload, filename=%s, purpose=%s',
721 $filename,
722 $purpose
723 );
724 Meow_MWAI_Logging::log( $debug );
725 }
726
727 $result = $this->simpleFileUpload( null, $base64, $filename, $purpose, $ttl, $target, $metadata );
728 }
729 else {
730 // Handle regular file upload
731 $file = $files['file'];
732 $purpose = isset( $params['purpose'] ) ? $params['purpose'] : 'analysis';
733 $ttl = isset( $params['ttl'] ) ? intval( $params['ttl'] ) : 3600;
734 $target = isset( $params['target'] ) ? $params['target'] : null;
735 $metadata = isset( $params['metadata'] ) ? $params['metadata'] : [];
736
737 if ( $this->debug ) {
738 $debug = sprintf(
739 'REST [SimpleFileUpload]: file upload, name=%s, purpose=%s',
740 $file['name'],
741 $purpose
742 );
743 Meow_MWAI_Logging::log( $debug );
744 }
745
746 $result = $this->simpleFileUpload( $file, null, null, $purpose, $ttl, $target, $metadata );
747 }
748
749 return new WP_REST_Response( [ 'success' => true, 'data' => $result ], 200 );
750 }
751 catch ( Exception $e ) {
752 return new WP_REST_Response( [ 'success' => false, 'message' => $e->getMessage() ], 500 );
753 }
754 }
755 #endregion
756
757 #region Simple API
758 /**
759 * Executes a vision query.`
760 *
761 * @param string $message The prompt for the AI.
762 * @param string $url The URL of the image to analyze.
763 * @param string|null $path The path to the image file. If provided, the image data will be read from this file.
764 * @param array $params Additional parameters for the AI query.
765 *
766 * @return string The result of the AI query.
767 */
768 public function simpleVisionQuery( $message, $url, $path = null, $params = [] ) {
769 global $mwai_core;
770 $ai_vision_default_env = $this->core->get_option( 'ai_vision_default_env' );
771 $ai_vision_default_model = $this->core->get_option( 'ai_vision_default_model' );
772 if ( empty( $ai_vision_default_model ) ) {
773 $ai_vision_default_model = MWAI_FALLBACK_MODEL_VISION;
774 }
775 $query = new Meow_MWAI_Query_Text( $message );
776 if ( !empty( $ai_vision_default_env ) ) {
777 $query->set_env_id( $ai_vision_default_env );
778 }
779 if ( !empty( $ai_vision_default_model ) ) {
780 $query->set_model( $ai_vision_default_model );
781 }
782 $query->inject_params( $params );
783 if ( isset( $params['image_remote_upload'] ) ) {
784 $query->image_remote_upload = $params['image_remote_upload'];
785 }
786 if ( !empty( $url ) ) {
787 $query->add_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'analysis' ) );
788 }
789 else if ( !empty( $path ) ) {
790 $query->add_file( Meow_MWAI_Query_DroppedFile::from_path( $path, 'analysis' ) );
791 }
792 $reply = $mwai_core->run_query( $query );
793 return $reply->result;
794 }
795
796 /**
797 * Executes a chatbot query.
798 * It will use the discussion if chatId is provided in the parameters.
799 *
800 * @param string $botId The ID of the chatbot.
801 * @param string $message The prompt for the AI.
802 * @param array $params Additional parameters for the AI query.
803 *
804 * @return string The result of the AI query.
805 */
806 public function simpleChatbotQuery( $botId, $message, $params = [], $onlyReply = true ) {
807 if ( !isset( $params['messages'] ) && isset( $params['chatId'] ) ) {
808 if ( $this->core->get_option( 'chatbot_discussions' ) && $this->discussions_module ) {
809 $discussion = $this->discussions_module->get_discussion( $botId, $params['chatId'] );
810 if ( !empty( $discussion ) ) {
811 $params['messages'] = $discussion['messages'];
812
813 // CRITICAL: Also pass the discussion metadata for Responses API support
814 // The chatbot module needs the previousResponseId from discussion's extra field
815 if ( !empty( $discussion['extra'] ) ) {
816 $extra = json_decode( $discussion['extra'], true );
817
818 // Check for both possible field names
819 $responseId = $extra['previousResponseId'] ?? $extra['responseId'] ?? null;
820 $responseDate = $extra['previousResponseDate'] ?? $extra['responseDate'] ?? null;
821
822 if ( !empty( $responseId ) ) {
823 // Check if the response ID is still valid (not older than 30 days)
824 $responseDateTimestamp = !empty( $responseDate ) ? strtotime( $responseDate ) : 0;
825 $thirtyDaysAgo = time() - ( 30 * 24 * 60 * 60 );
826
827 if ( $responseDateTimestamp > $thirtyDaysAgo ) {
828 // Pass the previousResponseId directly in params
829 // This will be picked up by inject_params in the query
830 $params['previousResponseId'] = $responseId;
831 }
832 }
833 }
834 }
835 }
836 else {
837 Meow_MWAI_Logging::log( 'The chatId was provided; but the discussions are not enabled.' );
838 }
839 }
840 $fileId = isset( $params['fileId'] ) ? $params['fileId'] : null;
841 $fileIds = isset( $params['fileIds'] ) ? $params['fileIds'] : [];
842 $data = $this->chatbot_module->chat_submit( $botId, $message, $fileId, $params, false, $fileIds );
843 return $onlyReply ? $data['reply'] : $data;
844 }
845
846 /**
847 * Executes a text query.
848 *
849 * @param string $message The prompt for the AI.
850 * @param array $params Additional parameters for the AI query.
851 *
852 * @return string The result of the AI query.
853 */
854 public function simpleTextQuery( $message, $params = [] ) {
855 global $mwai_core;
856 $query = new Meow_MWAI_Query_Text( $message );
857 $query->inject_params( $params );
858 $reply = $mwai_core->run_query( $query );
859 return $reply->result;
860 }
861
862 public function simpleFastTextQuery( $message, $params = [] ) {
863 global $mwai_core;
864 $query = new Meow_MWAI_Query_Text( $message );
865
866 // Use the Default (Fast) model and environment
867 $fastDefaultModel = $mwai_core->get_option( 'ai_fast_default_model' );
868 if ( !empty( $fastDefaultModel ) ) {
869 $query->set_model( $fastDefaultModel );
870 }
871
872 $fastDefaultEnv = $mwai_core->get_option( 'ai_fast_default_env' );
873 if ( !empty( $fastDefaultEnv ) ) {
874 $query->set_env_id( $fastDefaultEnv );
875 }
876
877 // Inject any additional params (which may override the defaults)
878 $query->inject_params( $params );
879
880 try {
881 $reply = $mwai_core->run_query( $query );
882 return $reply->result;
883 }
884 catch ( Exception $e ) {
885 // If Fast Model fails, try with default model
886 Meow_MWAI_Logging::warn( 'Fast Model failed: ' . $e->getMessage() . ' - Falling back to default model.' );
887
888 // Create a new query with default model/env
889 $fallbackQuery = new Meow_MWAI_Query_Text( $message );
890
891 $defaultModel = $mwai_core->get_option( 'ai_default_model' );
892 if ( !empty( $defaultModel ) ) {
893 $fallbackQuery->set_model( $defaultModel );
894 }
895
896 $defaultEnv = $mwai_core->get_option( 'ai_default_env' );
897 if ( !empty( $defaultEnv ) ) {
898 $fallbackQuery->set_env_id( $defaultEnv );
899 }
900
901 // Inject params again (except model/env which we just set)
902 $fallbackParams = $params;
903 unset( $fallbackParams['model'] );
904 unset( $fallbackParams['envId'] );
905 $fallbackQuery->inject_params( $fallbackParams );
906
907 $reply = $mwai_core->run_query( $fallbackQuery );
908 return $reply->result;
909 }
910 }
911
912 public function simpleImageQuery( $message, $params = [] ) {
913 global $mwai_core;
914 $query = new Meow_MWAI_Query_Image( $message );
915 $query->inject_params( $params );
916 $reply = $mwai_core->run_query( $query );
917 return $reply->result;
918 }
919
920 public function simpleImageEditQuery( $message, $mediaId, $params = [] ) {
921 global $mwai_core;
922 $query = new Meow_MWAI_Query_EditImage( $message );
923 $query->inject_params( $params );
924 $path = get_attached_file( $mediaId );
925 if ( empty( $path ) ) {
926 throw new Exception( 'The media cannot be found.' );
927 }
928 $query->add_file( Meow_MWAI_Query_DroppedFile::from_path( $path, 'analysis' ) );
929 $reply = $mwai_core->run_query( $query );
930 return $reply->result;
931 }
932
933 /**
934 * Generates an image relevant to the text.
935 */
936 public function imageQueryForMediaLibrary( $message, $params = [], $postId = null ) {
937 $query = new Meow_MWAI_Query_Image( $message );
938 $query->inject_params( $params );
939 $query->set_local_download( null );
940 $reply = $this->core->run_query( $query );
941 preg_match( '/\!\[Image\]\((.*?)\)/', $reply->result, $matches );
942 $url = $matches[1] ?? $reply->result;
943
944 // Check if the URL is already a WordPress attachment URL to avoid duplicates
945 $attachmentId = null;
946 $upload_dir = wp_upload_dir();
947 if ( strpos( $url, $upload_dir['baseurl'] ) === 0 ) {
948 // This is already a local WordPress upload, try to find the attachment ID
949 // First try by GUID
950 global $wpdb;
951 $attachmentId = $wpdb->get_var( $wpdb->prepare(
952 "SELECT ID FROM {$wpdb->posts} WHERE guid = %s AND post_type = 'attachment'",
953 $url
954 ) );
955
956 // If not found by GUID, try by attachment URL (more reliable)
957 if ( empty( $attachmentId ) ) {
958 $attachmentId = attachment_url_to_postid( $url );
959 }
960 }
961
962 // If not found or not a local URL, add it to the media library
963 if ( empty( $attachmentId ) ) {
964 $attachmentId = $this->core->add_image_from_url( $url, null, null, null, null, null, $postId );
965 if ( empty( $attachmentId ) ) {
966 throw new Exception( 'Could not add the image to the Media Library.' );
967 }
968 }
969
970 // TODO: We should create a nice title, caption, and alt.
971 $media = [
972 'id' => $attachmentId,
973 'url' => wp_get_attachment_url( $attachmentId ),
974 'title' => get_the_title( $attachmentId ),
975 'caption' => wp_get_attachment_caption( $attachmentId ),
976 'alt' => get_post_meta( $attachmentId, '_wp_attachment_image_alt', true )
977 ];
978 return $media;
979 }
980
981 /**
982 * Executes a query that will have to return a JSON result.
983 *
984 * @param string $message The prompt for the AI.
985 * @param array $params Additional parameters for the AI query.
986 *
987 * @return array The result of the AI query.
988 */
989 public function simpleJsonQuery( $message, $url = null, $path = null, $params = [] ) {
990 if ( !empty( $url ) || !empty( $path ) ) {
991 throw new Exception( 'The url and path are not supported yet by the simpleJsonQuery.' );
992 }
993 global $mwai_core;
994 $query = new Meow_MWAI_Query_Text( $message . "\nYour reply must be a formatted JSON." );
995 $query->inject_params( $params );
996 $query->set_response_format( 'json' );
997
998 // The JSON default env/model apply only when the caller didn't specify its
999 // own, so an explicit model/env passed in $params is always respected.
1000 if ( empty( $params['envId'] ) && empty( $params['model'] ) ) {
1001 $ai_json_default_env = $mwai_core->get_option( 'ai_json_default_env' );
1002 $ai_json_default_model = $mwai_core->get_option( 'ai_json_default_model' );
1003 if ( !empty( $ai_json_default_env ) ) {
1004 $query->set_env_id( $ai_json_default_env );
1005 }
1006 if ( !empty( $ai_json_default_model ) ) {
1007 $query->set_model( $ai_json_default_model );
1008 }
1009 }
1010
1011 // Only force the OpenAI JSON fallback as a last resort. When an env is set
1012 // (by the caller or the JSON default) but the model isn't, validate_env_model()
1013 // picks that environment's own model, so Anthropic/Google JSON queries no longer
1014 // 404 by sending a hardcoded OpenAI model name to the wrong provider.
1015 if ( empty( $query->model ) && empty( $query->envId ) ) {
1016 $query->set_model( MWAI_FALLBACK_MODEL_JSON );
1017 }
1018
1019 $reply = $mwai_core->run_query( $query );
1020 try {
1021 $json = json_decode( $reply->result, true );
1022 return $json;
1023 }
1024 catch ( Exception $e ) {
1025 throw new Exception( 'The result is not a valid JSON.' );
1026 }
1027 }
1028
1029 /**
1030 * Uploads a file to the system.
1031 *
1032 * @param array|null $file The file array from $_FILES.
1033 * @param string|null $base64 Base64 encoded file data.
1034 * @param string|null $filename The filename for base64 uploads.
1035 * @param string $purpose The purpose of the file upload ('analysis' or 'generated').
1036 * @param int $ttl Time to live in seconds. Default 3600 (1 hour).
1037 * @param string|null $target Target location: 'uploads' or 'library'.
1038 * @param array $metadata Additional metadata to store with the file.
1039 *
1040 * @return array Array with 'id' (refId) and 'url' of the uploaded file.
1041 */
1042 public function simpleFileUpload( $file = null, $base64 = null, $filename = null, $purpose = 'analysis', $ttl = 3600, $target = null, $metadata = [] ) {
1043 global $mwai_core;
1044
1045 if ( !$this->core->files ) {
1046 throw new Exception( 'Files module is not available.' );
1047 }
1048
1049 // Determine target from settings if not provided
1050 if ( empty( $target ) ) {
1051 $target = $this->core->get_option( 'image_local_upload', 'uploads' );
1052 }
1053
1054 try {
1055 if ( !empty( $base64 ) ) {
1056 // Handle base64 upload
1057 if ( empty( $filename ) ) {
1058 $filename = 'upload-' . time() . '.dat';
1059 }
1060
1061 // Validate filename extension for base64 uploads
1062 $validate = wp_check_filetype( $filename );
1063 if ( $validate['type'] == false ) {
1064 throw new Exception( 'File type is not allowed.' );
1065 }
1066
1067 // For base64 uploads, we need to decode and create a temp file first
1068 $binary = base64_decode( $base64 );
1069 if ( !$binary ) {
1070 throw new Exception( 'Invalid base64 data.' );
1071 }
1072
1073 // Create a temporary file
1074 $tmp_path = wp_tempnam( 'mwai-upload' );
1075 file_put_contents( $tmp_path, $binary );
1076
1077 // Use the regular upload method
1078 $refId = $this->core->files->upload_file(
1079 $tmp_path,
1080 $filename,
1081 $purpose,
1082 $metadata,
1083 null, // envId
1084 $target,
1085 $ttl
1086 );
1087
1088 // Clean up temp file if it was uploaded to library
1089 if ( $target === 'library' && file_exists( $tmp_path ) ) {
1090 @unlink( $tmp_path );
1091 }
1092
1093 $url = $this->core->files->get_url( $refId );
1094
1095 return [
1096 'id' => $refId,
1097 'url' => $url
1098 ];
1099 }
1100 else if ( !empty( $file ) && is_array( $file ) ) {
1101 // Handle regular file upload
1102 if ( !empty( $file['error'] ) ) {
1103 throw new Exception( 'File upload error: ' . $file['error'] );
1104 }
1105
1106 $refId = $this->core->files->upload_file(
1107 $file['tmp_name'],
1108 $file['name'],
1109 $purpose,
1110 $metadata,
1111 null, // envId
1112 $target,
1113 $ttl
1114 );
1115
1116 $url = $this->core->files->get_url( $refId );
1117
1118 return [
1119 'id' => $refId,
1120 'url' => $url
1121 ];
1122 }
1123 else {
1124 throw new Exception( 'Either a file or base64 data must be provided.' );
1125 }
1126 }
1127 catch ( Exception $e ) {
1128 throw new Exception( 'File upload failed: ' . $e->getMessage() );
1129 }
1130 }
1131
1132 /**
1133 * Executes an audio transcription query.
1134 *
1135 * @param string $url The URL of the audio file to transcribe.
1136 * @param string|null $path The path to the audio file. If provided, the audio data will be read from this file.
1137 * @param array $params Additional parameters for the transcription query.
1138 *
1139 * @return string The transcribed text.
1140 */
1141 public function simpleTranscribeAudio( $url = null, $path = null, $params = [] ) {
1142 global $mwai_core;
1143 $ai_audio_default_env = $this->core->get_option( 'ai_audio_default_env' );
1144 $ai_audio_default_model = $this->core->get_option( 'ai_audio_default_model' );
1145
1146 if ( empty( $ai_audio_default_model ) ) {
1147 // Only used when no model was picked in the settings. gpt-4o-mini-transcribe replaced
1148 // whisper-1 here because it costs half as much per second and transcribes better;
1149 // whisper-1 stays available, it is simply no longer what you get by not choosing.
1150 $ai_audio_default_model = 'gpt-4o-mini-transcribe';
1151 }
1152
1153 $query = new Meow_MWAI_Query_Transcribe();
1154
1155 if ( !empty( $ai_audio_default_env ) ) {
1156 $query->set_env_id( $ai_audio_default_env );
1157 }
1158 if ( !empty( $ai_audio_default_model ) ) {
1159 $query->set_model( $ai_audio_default_model );
1160 }
1161
1162 $query->inject_params( $params );
1163
1164 if ( !empty( $url ) ) {
1165 $query->add_file( Meow_MWAI_Query_DroppedFile::from_url( $url, 'analysis' ) );
1166 }
1167 else if ( !empty( $path ) ) {
1168 $query->add_file( Meow_MWAI_Query_DroppedFile::from_path( $path, 'analysis' ) );
1169 }
1170 else {
1171 throw new Exception( 'Either a URL or a path must be provided for the audio file.' );
1172 }
1173
1174 $reply = $mwai_core->run_query( $query );
1175 return $reply->result;
1176 }
1177 #endregion
1178
1179 #region Standard API
1180 /**
1181 * Checks if a text is safe or not.
1182 *
1183 * @param string $text The text to check.
1184 *
1185 * @return bool True if the text is safe, false otherwise.
1186 */
1187 public function moderationCheck( $text ) {
1188 global $mwai_core;
1189 $openai = Meow_MWAI_Engines_Factory::get_openai( $mwai_core );
1190 $res = $openai->moderate( $text );
1191 if ( !empty( $res ) && !empty( $res['results'] ) ) {
1192 return (bool) $res['results'][0]['flagged'];
1193 }
1194 }
1195 #endregion
1196
1197 #region Standard API (No REST API)
1198
1199 /**
1200 * Returns a stored discussion (read-only), or null if it doesn't exist.
1201 * Useful to check whether a chatId already has history, for instance to avoid
1202 * greeting a returning visitor twice when the front-end history is empty.
1203 *
1204 * @param string $botId The ID of the chatbot.
1205 * @param string $chatId The ID of the discussion.
1206 *
1207 * @return array|null The discussion (with 'messages' decoded), or null.
1208 */
1209 public function getDiscussion( $botId, $chatId ) {
1210 if ( !$this->discussions_module ) {
1211 return null;
1212 }
1213 return $this->discussions_module->get_discussion( $botId, $chatId );
1214 }
1215
1216 /**
1217 * Returns the status of AI Engine's capabilities.
1218 *
1219 * @return array {
1220 * @type bool $ai Whether at least one AI environment with an API key exists.
1221 * @type bool $mcp Whether the MCP module is enabled.
1222 * @type array $env_types List of available AI provider types (e.g. 'openai', 'anthropic').
1223 * }
1224 */
1225 public function getStatus() {
1226 $env_types = [];
1227 $ai_envs = $this->core->get_option( 'ai_envs' );
1228 if ( !empty( $ai_envs ) ) {
1229 foreach ( $ai_envs as $env ) {
1230 if ( !empty( $env['apikey'] ) && !in_array( $env['type'], $env_types ) ) {
1231 $env_types[] = $env['type'];
1232 }
1233 }
1234 }
1235 return [
1236 'ai' => !empty( $env_types ),
1237 'mcp' => (bool) $this->core->get_option( 'module_mcp' ),
1238 'env_types' => $env_types,
1239 ];
1240 }
1241
1242 /**
1243 * Whether at least one AI environment with an API key exists.
1244 *
1245 * @return bool
1246 */
1247 public function hasAI() {
1248 $ai_envs = $this->core->get_option( 'ai_envs' );
1249 if ( empty( $ai_envs ) ) {
1250 return false;
1251 }
1252 foreach ( $ai_envs as $env ) {
1253 if ( !empty( $env['apikey'] ) ) {
1254 return true;
1255 }
1256 }
1257 return false;
1258 }
1259
1260 /**
1261 * Whether the MCP module is enabled.
1262 *
1263 * @return bool
1264 */
1265 public function hasMCP() {
1266 return (bool) $this->core->get_option( 'module_mcp' );
1267 }
1268
1269 /**
1270 * Checks the status of the AI environments.
1271 *
1272 * @deprecated 3.5.0 Use getStatus(), hasAI(), or hasMCP() instead.
1273 * @return array The types of environments that are available.
1274 */
1275 public function checkStatus() {
1276 _deprecated_function( __METHOD__, '3.5.0', 'getStatus(), hasAI(), or hasMCP()' );
1277 $env_types = [];
1278 $ai_envs = $this->core->get_option( 'ai_envs' );
1279 if ( empty( $ai_envs ) ) {
1280 throw new Exception( 'There are no AI environments yet.' );
1281 }
1282 foreach ( $ai_envs as $env ) {
1283 if ( !empty( $env['apikey'] ) ) {
1284 if ( !in_array( $env['type'], $env_types ) ) {
1285 $env_types[] = $env['type'];
1286 }
1287 }
1288 }
1289 if ( empty( $env_types ) ) {
1290 throw new Exception( 'There are no AI environments with an API key yet.' );
1291 }
1292 return $env_types;
1293 }
1294
1295 /**
1296 * Get function name by ID
1297 */
1298 private function get_function_name_by_id( $funcId ) {
1299 $function = MeowPro_MWAI_FunctionAware::get_function( 'code-engine', $funcId );
1300 if ( $function && isset( $function->name ) ) {
1301 return $function->name;
1302 }
1303 return null;
1304 }
1305 #endregion
1306 }
1307