PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / trunk
AI Builder – Generate pages, blocks, images & translate with AI vtrunk
2.7.9 2.7.8 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.3.10 2.3.11 All 121 releases
ai-builder / includes / class-agent-execution-service.php

class-agent-execution-service.php in AI Builder – Generate pages, blocks, images & translate with AI trunk, at includes/class-agent-execution-service.php

455 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Agent Execution Service
4 *
5 * Executes whitelisted WordPress REST API routes internally
6 * without making external HTTP requests.
7 *
8 * @package AI_Builder
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 class AIBUI_Agent_Execution_Service
16 {
17 /**
18 * Security service instance
19 *
20 * @var AIBUI_Agent_Security_Service
21 */
22 private $security;
23
24 /**
25 * Discovery service instance
26 *
27 * @var AIBUI_Agent_Discovery_Service
28 */
29 private $discovery;
30
31 /**
32 * Execution log for debugging
33 *
34 * @var array
35 */
36 private $execution_log = array();
37
38 /**
39 * Maximum executions per request (prevent infinite loops)
40 */
41 const MAX_EXECUTIONS = 20;
42
43 /**
44 * Current execution count
45 *
46 * @var int
47 */
48 private $execution_count = 0;
49
50 /**
51 * Constructor
52 *
53 * @param AIBUI_Agent_Security_Service $security
54 * @param AIBUI_Agent_Discovery_Service $discovery
55 */
56 public function __construct(
57 AIBUI_Agent_Security_Service $security,
58 AIBUI_Agent_Discovery_Service $discovery
59 ) {
60 $this->security = $security;
61 $this->discovery = $discovery;
62 }
63
64 /**
65 * Execute a tool call from the AI
66 *
67 * @param string $tool_name Name of the tool (generated from route)
68 * @param array $params Parameters for the tool
69 * @return array Result with 'success', 'data' or 'error'
70 */
71 public function execute_tool($tool_name, array $params = array())
72 {
73 // Check execution limit
74 if ($this->execution_count >= self::MAX_EXECUTIONS) {
75 return $this->error_response(
76 'EXECUTION_LIMIT_REACHED',
77 'Maximum number of tool executions reached in this request'
78 );
79 }
80 $this->execution_count++;
81
82 // Validate the tool exists and is whitelisted
83 $route_meta = $this->security->validate_tool($tool_name);
84
85 if (!$route_meta) {
86 return $this->error_response(
87 'TOOL_NOT_ALLOWED',
88 "Tool '{$tool_name}' is not available or not whitelisted"
89 );
90 }
91
92 $route = $route_meta['route'];
93 $method = $route_meta['method'];
94
95 // Log execution attempt
96 $this->log_execution('attempt', $tool_name, array(
97 'route' => $route,
98 'method' => $method,
99 'params' => $params,
100 ));
101
102 // Execute the route
103 return $this->execute_route($route, $method, $params);
104 }
105
106 /**
107 * Execute a REST API route internally
108 *
109 * @param string $route Route path
110 * @param string $method HTTP method
111 * @param array $params Parameters
112 * @return array Result
113 */
114 public function execute_route($route, $method, array $params = array())
115 {
116 // Security check 1: Is route whitelisted?
117 if (!$this->security->is_route_whitelisted($route, $method)) {
118 // Try to match parameterized route
119 $matched_route = $this->match_parameterized_route($route, $method);
120 if (!$matched_route || !$this->security->is_route_whitelisted($matched_route, $method)) {
121 return $this->error_response(
122 'ROUTE_NOT_WHITELISTED',
123 "Route '{$method} {$route}' is not whitelisted for AI agent access"
124 );
125 }
126 }
127
128 // Security check 2: Is route forbidden for this HTTP method?
129 if ($this->discovery->is_forbidden_route($route, $method)) {
130 return $this->error_response(
131 'ROUTE_FORBIDDEN',
132 "Route '{$route}' is forbidden for security reasons"
133 );
134 }
135
136 // Build the request
137 $request = $this->build_request($route, $method, $params);
138
139 // Execute via REST API
140 try {
141 $response = rest_do_request($request);
142 return $this->process_response($response);
143 } catch (Exception $e) {
144 $this->log_execution('error', $route, array(
145 'method' => $method,
146 'error' => $e->getMessage(),
147 ));
148
149 return $this->error_response(
150 'EXECUTION_FAILED',
151 'Failed to execute request: ' . $e->getMessage()
152 );
153 }
154 }
155
156 /**
157 * Build a WP_REST_Request object
158 *
159 * @param string $route Route path
160 * @param string $method HTTP method
161 * @param array $params Parameters
162 * @return WP_REST_Request
163 */
164 private function build_request($route, $method, array $params)
165 {
166 $request = new WP_REST_Request($method, $route);
167
168 // Separate URL params from body params
169 $url_params = array();
170 $body_params = array();
171
172 // Extract URL parameters from route pattern
173 $url_param_names = $this->extract_url_param_names($route);
174
175 foreach ($params as $key => $value) {
176 if (in_array($key, $url_param_names)) {
177 $url_params[$key] = $value;
178 } else {
179 $body_params[$key] = $value;
180 }
181 }
182
183 // Set URL parameters
184 foreach ($url_params as $key => $value) {
185 $request->set_url_params(array($key => $value));
186 }
187
188 // Set query or body parameters based on method
189 if (in_array($method, array('GET', 'HEAD', 'DELETE'))) {
190 $request->set_query_params($body_params);
191 } else {
192 $request->set_body_params($body_params);
193 }
194
195 // Set content type for POST/PUT/PATCH
196 if (in_array($method, array('POST', 'PUT', 'PATCH'))) {
197 $request->set_header('Content-Type', 'application/json');
198 }
199
200 return $request;
201 }
202
203 /**
204 * Extract URL parameter names from route pattern
205 *
206 * @param string $route
207 * @return array
208 */
209 private function extract_url_param_names($route)
210 {
211 $names = array();
212 if (preg_match_all('/\(\?P<([^>]+)>[^)]+\)/', $route, $matches)) {
213 $names = $matches[1];
214 }
215 return $names;
216 }
217
218 /**
219 * Match a concrete path to a parameterized route pattern
220 *
221 * @param string $path Concrete path (e.g., /wp/v2/posts/123)
222 * @param string $method HTTP method
223 * @return string|null Matched route pattern or null
224 */
225 private function match_parameterized_route($path, $method)
226 {
227 $routes = $this->security->get_enabled_routes();
228
229 foreach ($routes as $route) {
230 if ($route['method'] !== strtoupper($method)) {
231 continue;
232 }
233
234 // Convert route pattern to regex for matching
235 $pattern = '#^' . $route['route'] . '$#';
236 if (preg_match($pattern, $path)) {
237 return $route['route'];
238 }
239 }
240
241 return null;
242 }
243
244 /**
245 * Process the REST API response
246 *
247 * @param WP_REST_Response $response
248 * @return array
249 */
250 private function process_response($response)
251 {
252 $data = $response->get_data();
253 $status = $response->get_status();
254 $headers = $response->get_headers();
255
256 // Check for errors
257 if ($status >= 400) {
258 $error_message = 'Request failed';
259 $error_code = 'REQUEST_FAILED';
260
261 if (is_wp_error($data)) {
262 $error_message = $data->get_error_message();
263 $error_code = $data->get_error_code();
264 } elseif (isset($data['message'])) {
265 $error_message = $data['message'];
266 $error_code = isset($data['code']) ? $data['code'] : 'API_ERROR';
267 }
268
269 $this->log_execution('api_error', 'response', array(
270 'status' => $status,
271 'error' => $error_message,
272 ));
273
274 return array(
275 'success' => false,
276 'error' => array(
277 'code' => $error_code,
278 'message' => $error_message,
279 'status' => $status,
280 ),
281 );
282 }
283
284 // Log success
285 $this->log_execution('success', 'response', array(
286 'status' => $status,
287 'data_type' => gettype($data),
288 ));
289
290 // Truncate large responses for AI context
291 $processed_data = $this->truncate_response_data($data);
292
293 return array(
294 'success' => true,
295 'data' => $processed_data,
296 'meta' => array(
297 'status' => $status,
298 'total_results' => $this->count_results($data),
299 ),
300 );
301 }
302
303 /**
304 * Truncate large response data to prevent token overflow
305 *
306 * @param mixed $data
307 * @param int $max_items Maximum items in arrays
308 * @return mixed
309 */
310 private function truncate_response_data($data, $max_items = 10)
311 {
312 if (is_array($data)) {
313 // Check if it's a list of items
314 if (isset($data[0]) && is_array($data[0])) {
315 $total = count($data);
316 $data = array_slice($data, 0, $max_items);
317
318 // Add truncation notice
319 if ($total > $max_items) {
320 $data[] = array(
321 '_truncated' => true,
322 '_message' => sprintf('Showing %d of %d total results', $max_items, $total),
323 );
324 }
325 }
326
327 // Recursively process nested arrays
328 foreach ($data as $key => $value) {
329 if (is_array($value)) {
330 // Remove potentially large/unnecessary fields
331 $fields_to_remove = array(
332 'content', 'rendered', 'raw', 'guid', '_links',
333 'yoast_head', 'yoast_head_json',
334 );
335
336 foreach ($fields_to_remove as $field) {
337 if (isset($value[$field]) && is_string($value[$field]) && strlen($value[$field]) > 500) {
338 $value[$field] = substr($value[$field], 0, 500) . '... [truncated]';
339 }
340 }
341
342 $data[$key] = $value;
343 }
344 }
345 }
346
347 return $data;
348 }
349
350 /**
351 * Count results in response data
352 *
353 * @param mixed $data
354 * @return int|null
355 */
356 private function count_results($data)
357 {
358 if (is_array($data) && isset($data[0])) {
359 return count($data);
360 }
361 return null;
362 }
363
364 /**
365 * Create error response
366 *
367 * @param string $code
368 * @param string $message
369 * @return array
370 */
371 private function error_response($code, $message)
372 {
373 return array(
374 'success' => false,
375 'error' => array(
376 'code' => $code,
377 'message' => $message,
378 ),
379 );
380 }
381
382 /**
383 * Log execution for debugging
384 *
385 * @param string $type
386 * @param string $context
387 * @param array $data
388 */
389 private function log_execution($type, $context, array $data = array())
390 {
391 $this->execution_log[] = array(
392 'type' => $type,
393 'context' => $context,
394 'data' => $data,
395 'timestamp' => microtime(true),
396 );
397 }
398
399 /**
400 * Get execution log
401 *
402 * @return array
403 */
404 public function get_execution_log()
405 {
406 return $this->execution_log;
407 }
408
409 /**
410 * Reset execution counter (for new request)
411 */
412 public function reset_execution_count()
413 {
414 $this->execution_count = 0;
415 $this->execution_log = array();
416 }
417
418 /**
419 * Get current execution count
420 *
421 * @return int
422 */
423 public function get_execution_count()
424 {
425 return $this->execution_count;
426 }
427
428 /**
429 * Execute multiple tool calls in sequence
430 *
431 * @param array $tool_calls Array of ['tool_name' => ..., 'params' => ...]
432 * @return array Results for each tool call
433 */
434 public function execute_tool_calls(array $tool_calls)
435 {
436 $results = array();
437
438 foreach ($tool_calls as $index => $call) {
439 if (!isset($call['tool_name'])) {
440 $results[$index] = $this->error_response(
441 'INVALID_TOOL_CALL',
442 'Tool call missing tool_name'
443 );
444 continue;
445 }
446
447 $params = isset($call['params']) ? $call['params'] : array();
448 $results[$index] = $this->execute_tool($call['tool_name'], $params);
449 }
450
451 return $results;
452 }
453 }
454
455