PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.7.1
AI Builder – Generate pages, blocks, images & translate with AI v2.7.1
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-chat-handler.php

class-agent-chat-handler.php in AI Builder – Generate pages, blocks, images & translate with AI 2.7.1, at includes/class-agent-chat-handler.php

692 lines 22.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Agent Chat Handler
4 *
5 * Main chat handler that acts as a proxy between the frontend and
6 * the external Node.js API. Manages tool execution loop.
7 *
8 * @package AI_Builder
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 class AIBUI_Agent_Chat_Handler
16 {
17 /**
18 * External API base URL
19 */
20 const API_BASE_URL = 'https://api.wordpress-ai-builder.com/api';
21 // const API_BASE_URL = 'http://localhost:8080/api';
22
23 /**
24 * Agent chat endpoint
25 */
26 const AGENT_ENDPOINT = '/agent/chat';
27
28 /**
29 * Maximum iterations in tool execution loop
30 */
31 const MAX_LOOP_ITERATIONS = 6;
32
33 /**
34 * Request timeout in seconds
35 */
36 const REQUEST_TIMEOUT = 120;
37
38 /**
39 * Discovery service
40 *
41 * @var AIBUI_Agent_Discovery_Service
42 */
43 private $discovery;
44
45 /**
46 * Security service
47 *
48 * @var AIBUI_Agent_Security_Service
49 */
50 private $security;
51
52 /**
53 * Execution service
54 *
55 * @var AIBUI_Agent_Execution_Service
56 */
57 private $executor;
58
59 /**
60 * Conversation history
61 *
62 * @var array
63 */
64 private $conversation_history = array();
65
66 /**
67 * Constructor
68 */
69 public function __construct()
70 {
71 $this->discovery = new AIBUI_Agent_Discovery_Service();
72 $this->security = new AIBUI_Agent_Security_Service($this->discovery);
73 $this->executor = new AIBUI_Agent_Execution_Service($this->security, $this->discovery);
74
75 // Register AJAX handlers
76 add_action('wp_ajax_aibui_agent_chat', array($this, 'handle_chat_request'));
77 add_action('wp_ajax_aibui_agent_api_proxy', array($this, 'handle_api_proxy'));
78 add_action('wp_ajax_aibui_agent_execute_tool', array($this, 'handle_execute_tool'));
79 add_action('wp_ajax_aibui_agent_get_routes', array($this, 'handle_get_routes'));
80 add_action('wp_ajax_aibui_agent_update_routes', array($this, 'handle_update_routes'));
81 add_action('wp_ajax_aibui_agent_get_stats', array($this, 'handle_get_stats'));
82 add_action('wp_ajax_aibui_agent_reset_routes', array($this, 'handle_reset_routes'));
83 }
84
85 /**
86 * Handle API proxy request - just forwards to external API
87 * This allows the frontend to manage the agentic loop for real-time updates
88 */
89 public function handle_api_proxy()
90 {
91 // Verify nonce
92 if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) {
93 wp_send_json_error(array('message' => 'Security check failed'), 403);
94 }
95
96 // Check permissions
97 if (!current_user_can('manage_options')) {
98 wp_send_json_error(array('message' => 'Insufficient permissions'), 403);
99 }
100
101 // Get JWT token
102 $jwt_token = get_option('aibui_jwt_token', '');
103 if (empty($jwt_token)) {
104 wp_send_json_error(array('message' => 'Authentication required. Please sign in first.'), 401);
105 }
106
107 // Get payload from request
108 // Note: tool_results are now embedded in messages array as user messages
109 // with tool_result content blocks, so we don't handle them separately
110 $messages = array();
111 if (isset($_POST['messages']) && is_string($_POST['messages'])) {
112 $decoded = json_decode(wp_unslash($_POST['messages']), true);
113 if (is_array($decoded)) {
114 $messages = $decoded;
115 }
116 }
117
118 // Build available tools from whitelisted routes (strip _meta)
119 $tools_with_meta = $this->security->get_enabled_tools();
120 $tools = array_map(function($tool) {
121 unset($tool['_meta']);
122 return $tool;
123 }, $tools_with_meta);
124
125 // Build payload - messages already contain tool_result blocks when needed
126 $payload = array(
127 'messages' => $messages,
128 'tools_schema' => $tools,
129 );
130
131 // Call external API
132 $api_response = $this->call_external_api($jwt_token, $payload);
133
134 if ($api_response['success']) {
135 wp_send_json_success($api_response['data']);
136 } else {
137 wp_send_json_error($api_response['error'], $api_response['status'] ?? 500);
138 }
139 }
140
141 /**
142 * Handle tool execution request
143 * Executes a single tool and returns the result
144 */
145 public function handle_execute_tool()
146 {
147 // Verify nonce
148 if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) {
149 wp_send_json_error(array('message' => 'Security check failed'), 403);
150 }
151
152 // Check permissions
153 if (!current_user_can('manage_options')) {
154 wp_send_json_error(array('message' => 'Insufficient permissions'), 403);
155 }
156
157 // Get tool info
158 $tool_use_id = isset($_POST['tool_use_id']) ? sanitize_text_field(wp_unslash($_POST['tool_use_id'])) : '';
159 $tool_name = isset($_POST['tool_name']) ? sanitize_text_field(wp_unslash($_POST['tool_name'])) : '';
160 $tool_params = array();
161
162 if (isset($_POST['tool_params']) && is_string($_POST['tool_params'])) {
163 $decoded = json_decode(wp_unslash($_POST['tool_params']), true);
164 if (is_array($decoded)) {
165 $tool_params = $decoded;
166 }
167 }
168
169 if (empty($tool_name)) {
170 wp_send_json_error(array('message' => 'Tool name is required'), 400);
171 }
172
173 // Execute the tool
174 $tool_call = array(
175 'id' => $tool_use_id,
176 'name' => $tool_name,
177 'input' => $tool_params,
178 );
179 $executed = $this->execute_tool_calls(array($tool_call));
180
181 // Get result content
182 $result_content = '';
183 if (!empty($executed) && isset($executed[0]['result'])) {
184 $result = $executed[0]['result'];
185 $result_content = is_string($result) ? $result : wp_json_encode($result);
186 }
187
188 wp_send_json_success(array(
189 'tool_use_id' => $tool_use_id,
190 'tool_name' => $tool_name,
191 'content' => $result_content,
192 'execution_log' => $executed,
193 ));
194 }
195
196 /**
197 * Handle chat request from frontend
198 */
199 public function handle_chat_request()
200 {
201 // Verify nonce
202 if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) {
203 wp_send_json_error(array('message' => 'Security check failed'), 403);
204 }
205
206 // Check permissions
207 if (!current_user_can('manage_options')) {
208 wp_send_json_error(array('message' => 'Insufficient permissions'), 403);
209 }
210
211 // Get user message
212 $message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : '';
213 if (empty($message)) {
214 wp_send_json_error(array('message' => 'Message is required'), 400);
215 }
216
217 // Get conversation history (optional)
218 $history = array();
219 if (isset($_POST['history']) && is_string($_POST['history'])) {
220 $decoded = json_decode(wp_unslash($_POST['history']), true);
221 if (is_array($decoded)) {
222 $history = $decoded;
223 }
224 }
225
226 // Execute chat
227 $result = $this->process_chat($message, $history);
228
229 if ($result['success']) {
230 wp_send_json_success($result['data']);
231 } else {
232 wp_send_json_error($result['error'], $result['status'] ?? 500);
233 }
234 }
235
236 /**
237 * Process chat message and execute tool loop
238 *
239 * @param string $message User message
240 * @param array $history Conversation history
241 * @return array
242 */
243 public function process_chat($message, array $history = array())
244 {
245 // Get JWT token
246 $jwt_token = get_option('aibui_jwt_token', '');
247 if (empty($jwt_token)) {
248 return array(
249 'success' => false,
250 'error' => array('message' => 'Authentication required. Please sign in first.'),
251 'status' => 401,
252 );
253 }
254
255 // Build available tools from whitelisted routes
256 $tools_with_meta = $this->security->get_enabled_tools();
257
258 // Strip _meta from tools before sending to API (Claude doesn't accept extra fields)
259 $tools = array_map(function($tool) {
260 unset($tool['_meta']);
261 return $tool;
262 }, $tools_with_meta);
263
264 // Prepare system context
265 $system_context = $this->build_system_context();
266
267 // Initialize loop variables
268 $iterations = 0;
269 $final_response = null;
270 $final_credits = null;
271 $pending_tool_results = array(); // Tool results to send in next request
272
273 // Build initial messages array from history
274 $messages = array();
275 foreach ($history as $hist_item) {
276 if (isset($hist_item['role']) && isset($hist_item['content'])) {
277 $messages[] = array(
278 'role' => $hist_item['role'],
279 'content' => $hist_item['content'],
280 );
281 }
282 }
283
284 // Add current user message
285 if (!empty($message)) {
286 $messages[] = array(
287 'role' => 'user',
288 'content' => $message,
289 );
290 }
291
292 // Main agent loop
293 while ($iterations < self::MAX_LOOP_ITERATIONS) {
294 $iterations++;
295
296 // Build API request payload
297 $payload = array(
298 'messages' => $messages,
299 'tools_schema' => $tools,
300 );
301
302 // Add tool_results if we have results from previous iteration
303 if (!empty($pending_tool_results)) {
304 $payload['tool_results'] = $pending_tool_results;
305 $pending_tool_results = array(); // Clear after adding to payload
306 }
307
308 // Call external API
309 $api_response = $this->call_external_api($jwt_token, $payload);
310
311 if (!$api_response['success']) {
312 return $api_response;
313 }
314
315 $response_data = $api_response['data'];
316 $credits_left = $response_data['creditsLeft'] ?? $api_response['credits'] ?? null;
317
318 // Check if AI wants to use tools (type === 'tool_request')
319 if (isset($response_data['type']) && $response_data['type'] === 'tool_request') {
320
321 // 1. Store assistant_message in messages for proper conversation flow
322 if (isset($response_data['assistant_message'])) {
323 $messages[] = array(
324 'role' => 'assistant',
325 'content' => $response_data['assistant_message'],
326 );
327 }
328
329 // 2. Get tools to execute - can be single tool or array
330 $tools_to_execute = array();
331
332 // New format: tools array
333 if (isset($response_data['tools']) && is_array($response_data['tools'])) {
334 $tools_to_execute = $response_data['tools'];
335 }
336 // Legacy format: single tool
337 elseif (isset($response_data['tool_use_id'])) {
338 $tools_to_execute[] = array(
339 'tool_use_id' => $response_data['tool_use_id'],
340 'tool_name' => $response_data['tool_name'] ?? '',
341 'tool_params' => $response_data['tool_params'] ?? array(),
342 );
343 }
344
345 // 3. Execute ALL tools and collect results with REAL tool_use_id
346 foreach ($tools_to_execute as $tool_request) {
347 $tool_use_id = $tool_request['tool_use_id'] ?? '';
348 $tool_name = $tool_request['tool_name'] ?? '';
349 $tool_params = $tool_request['tool_params'] ?? array();
350
351 // Execute the tool
352 $tool_call = array(
353 'id' => $tool_use_id,
354 'name' => $tool_name,
355 'input' => $tool_params,
356 );
357 $executed = $this->execute_tool_calls(array($tool_call));
358
359 // Get result content
360 $result_content = '';
361 if (!empty($executed) && isset($executed[0]['result'])) {
362 $result = $executed[0]['result'];
363 $result_content = is_string($result) ? $result : wp_json_encode($result);
364 }
365
366 // Add to pending results with the REAL tool_use_id from Claude
367 $pending_tool_results[] = array(
368 'tool_use_id' => $tool_use_id,
369 'content' => $result_content,
370 );
371 }
372
373 // Continue loop to send results back
374 continue;
375 }
376
377 // No more tool calls, we have the final response (type === 'text')
378 $final_response = $response_data;
379 $final_credits = $credits_left;
380 break;
381 }
382
383 if ($iterations >= self::MAX_LOOP_ITERATIONS) {
384 return array(
385 'success' => false,
386 'error' => array('message' => 'Maximum tool execution iterations reached'),
387 'status' => 500,
388 );
389 }
390
391 return array(
392 'success' => true,
393 'data' => array(
394 'response' => $final_response['content'] ?? '',
395 'tool_executions' => $this->executor->get_execution_log(),
396 'iterations' => $iterations,
397 'credits' => $final_credits ?? null,
398 ),
399 );
400 }
401
402 /**
403 * Call the external Node.js API
404 *
405 * @param string $jwt_token
406 * @param array $payload
407 * @return array
408 */
409 private function call_external_api($jwt_token, array $payload)
410 {
411 $url = self::API_BASE_URL . self::AGENT_ENDPOINT;
412
413 $response = wp_remote_post($url, array(
414 'timeout' => self::REQUEST_TIMEOUT,
415 'headers' => array(
416 'Authorization' => 'Bearer ' . $jwt_token,
417 'Content-Type' => 'application/json',
418 ),
419 'body' => wp_json_encode($payload),
420 ));
421
422 if (is_wp_error($response)) {
423 return array(
424 'success' => false,
425 'error' => array('message' => 'API request failed: ' . $response->get_error_message()),
426 'status' => 500,
427 );
428 }
429
430 $status_code = wp_remote_retrieve_response_code($response);
431 $body = wp_remote_retrieve_body($response);
432 $data = json_decode($body, true);
433
434 // Extract remaining credits if provided by API (check creditsLeft first - Node.js API format)
435 $credits = null;
436 if (is_array($data)) {
437 if (isset($data['creditsLeft'])) {
438 $credits = (int) $data['creditsLeft'];
439 } elseif (isset($data['credits'])) {
440 $credits = (int) $data['credits'];
441 } elseif (isset($data['remaining_credits'])) {
442 $credits = (int) $data['remaining_credits'];
443 } elseif (isset($data['meta']['credits'])) {
444 $credits = (int) $data['meta']['credits'];
445 }
446 }
447
448 // Handle errors, including insufficient credits (402)
449 if ($status_code >= 400) {
450 // Log the raw response for debugging
451
452 $error_message = isset($data['message']) ? $data['message'] : 'API error';
453
454 // Include more details in error for debugging
455 if (empty($data['message']) && !empty($body)) {
456 $error_message = 'API error';
457 // $error_message = 'API error: ' . substr($body, 0, 500);
458 }
459
460 // Special handling for 402 Payment Required (not enough credits)
461 if ($status_code === 402) {
462 $credits_url = admin_url('admin.php?page=aibui-credits');
463 $error_message = __('You do not have enough AI credits to run this request. Please top up your credits on the Credits page.', 'ai-builder');
464
465 return array(
466 'success' => false,
467 'error' => array(
468 'message' => $error_message,
469 'code' => $status_code,
470 'credits' => $credits,
471 'credits_url' => $credits_url,
472 ),
473 'status' => $status_code,
474 'credits' => $credits,
475 );
476 }
477
478 return array(
479 'success' => false,
480 'error' => array(
481 'message' => $error_message,
482 'code' => $status_code,
483 'credits' => $credits,
484 ),
485 'status' => $status_code,
486 'credits' => $credits,
487 );
488 }
489
490 return array(
491 'success' => true,
492 'data' => $data,
493 'credits' => $credits,
494 );
495 }
496
497 /**
498 * Execute tool calls requested by AI
499 *
500 * @param array $tool_calls
501 * @return array Results
502 */
503 private function execute_tool_calls(array $tool_calls)
504 {
505 $results = array();
506
507 foreach ($tool_calls as $call) {
508 $tool_name = isset($call['name']) ? $call['name'] : '';
509 $tool_id = isset($call['id']) ? $call['id'] : uniqid('tool_');
510 $input = isset($call['input']) ? $call['input'] : array();
511
512 // Parse input if it's a string
513 if (is_string($input)) {
514 $decoded = json_decode($input, true);
515 $input = is_array($decoded) ? $decoded : array();
516 }
517
518 // Execute the tool
519 $result = $this->executor->execute_tool($tool_name, $input);
520
521 $results[] = array(
522 'tool_use_id' => $tool_id,
523 'tool_name' => $tool_name,
524 'result' => $result,
525 );
526 }
527
528 return $results;
529 }
530
531 /**
532 * Build system context for the AI
533 *
534 * @return string
535 */
536 private function build_system_context()
537 {
538 $site_name = get_bloginfo('name');
539 $site_url = get_site_url();
540 $wp_version = get_bloginfo('version');
541 $current_user = wp_get_current_user();
542
543 $context = "You are an AI assistant helping to manage a WordPress website.\n\n";
544 $context .= "Site Information:\n";
545 $context .= "- Site Name: {$site_name}\n";
546 $context .= "- Site URL: {$site_url}\n";
547 $context .= "- WordPress Version: {$wp_version}\n";
548 $context .= "- Current User: {$current_user->display_name} ({$current_user->user_email})\n\n";
549
550 $context .= "You have access to WordPress REST API tools to help manage content.\n";
551 $context .= "Always verify actions with the user before making changes.\n";
552 $context .= "When listing items, limit results to be concise.\n";
553 $context .= "Provide helpful summaries of data retrieved.\n";
554
555 return $context;
556 }
557
558 /**
559 * Get site information
560 *
561 * @return array
562 */
563 private function get_site_info()
564 {
565 return array(
566 'name' => get_bloginfo('name'),
567 'url' => get_site_url(),
568 'admin_url' => admin_url(),
569 'wp_version' => get_bloginfo('version'),
570 'language' => get_bloginfo('language'),
571 'timezone' => wp_timezone_string(),
572 );
573 }
574
575 /**
576 * Handle get routes request (for settings page)
577 */
578 public function handle_get_routes()
579 {
580 if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) {
581 wp_send_json_error(array('message' => 'Security check failed'), 403);
582 }
583
584 if (!current_user_can('manage_options')) {
585 wp_send_json_error(array('message' => 'Insufficient permissions'), 403);
586 }
587
588 $routes = $this->security->get_routes_with_status();
589 wp_send_json_success($routes);
590 }
591
592 /**
593 * Handle update routes request
594 */
595 public function handle_update_routes()
596 {
597 if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) {
598 wp_send_json_error(array('message' => 'Security check failed'), 403);
599 }
600
601 if (!current_user_can('manage_options')) {
602 wp_send_json_error(array('message' => 'Insufficient permissions'), 403);
603 }
604
605 $enabled_routes = array();
606 if (isset($_POST['enabled_routes']) && is_string($_POST['enabled_routes'])) {
607 $decoded = json_decode(wp_unslash($_POST['enabled_routes']), true);
608 if (is_array($decoded)) {
609 $enabled_routes = array_map('sanitize_text_field', $decoded);
610 }
611 }
612
613 $success = $this->security->update_whitelist($enabled_routes);
614
615 if ($success) {
616 wp_send_json_success(array('message' => 'Routes updated successfully'));
617 } else {
618 wp_send_json_error(array('message' => 'Failed to update routes'), 500);
619 }
620 }
621
622 /**
623 * Handle get stats request
624 */
625 public function handle_get_stats()
626 {
627 if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) {
628 wp_send_json_error(array('message' => 'Security check failed'), 403);
629 }
630
631 if (!current_user_can('manage_options')) {
632 wp_send_json_error(array('message' => 'Insufficient permissions'), 403);
633 }
634
635 $stats = $this->security->get_stats();
636 wp_send_json_success($stats);
637 }
638
639 /**
640 * Handle reset routes request
641 */
642 public function handle_reset_routes()
643 {
644 if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) {
645 wp_send_json_error(array('message' => 'Security check failed'), 403);
646 }
647
648 if (!current_user_can('manage_options')) {
649 wp_send_json_error(array('message' => 'Insufficient permissions'), 403);
650 }
651
652 $success = $this->security->reset_to_defaults();
653
654 if ($success) {
655 wp_send_json_success(array('message' => 'Routes reset to defaults'));
656 } else {
657 wp_send_json_error(array('message' => 'Failed to reset routes'), 500);
658 }
659 }
660
661 /**
662 * Get the security service instance
663 *
664 * @return AIBUI_Agent_Security_Service
665 */
666 public function get_security_service()
667 {
668 return $this->security;
669 }
670
671 /**
672 * Get the discovery service instance
673 *
674 * @return AIBUI_Agent_Discovery_Service
675 */
676 public function get_discovery_service()
677 {
678 return $this->discovery;
679 }
680
681 /**
682 * Get the execution service instance
683 *
684 * @return AIBUI_Agent_Execution_Service
685 */
686 public function get_execution_service()
687 {
688 return $this->executor;
689 }
690 }
691
692