PluginProbe
AI Builder – Generate pages, blocks, images & translate with AI / 2.3.0
AI Builder – Generate pages, blocks, images & translate with AI v2.3.0
2.7.10 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 All 122 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.3.0, at includes/class-agent-chat-handler.php

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