discovery = new AIBUI_Agent_Discovery_Service(); $this->security = new AIBUI_Agent_Security_Service($this->discovery); $this->executor = new AIBUI_Agent_Execution_Service($this->security, $this->discovery); // Register AJAX handlers add_action('wp_ajax_aibui_agent_chat', array($this, 'handle_chat_request')); add_action('wp_ajax_aibui_agent_api_proxy', array($this, 'handle_api_proxy')); add_action('wp_ajax_aibui_agent_execute_tool', array($this, 'handle_execute_tool')); add_action('wp_ajax_aibui_agent_get_routes', array($this, 'handle_get_routes')); add_action('wp_ajax_aibui_agent_update_routes', array($this, 'handle_update_routes')); add_action('wp_ajax_aibui_agent_get_stats', array($this, 'handle_get_stats')); add_action('wp_ajax_aibui_agent_reset_routes', array($this, 'handle_reset_routes')); } /** * Handle API proxy request - just forwards to external API * This allows the frontend to manage the agentic loop for real-time updates */ public function handle_api_proxy() { // Verify nonce if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) { wp_send_json_error(array('message' => 'Security check failed'), 403); } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions'), 403); } // Get JWT token $jwt_token = get_option('aibui_jwt_token', ''); if (empty($jwt_token)) { wp_send_json_error(array('message' => 'Authentication required. Please sign in first.'), 401); } // Get payload from request // Note: tool_results are now embedded in messages array as user messages // with tool_result content blocks, so we don't handle them separately $messages = array(); if (isset($_POST['messages']) && is_string($_POST['messages'])) { $decoded = json_decode(wp_unslash($_POST['messages']), true); if (is_array($decoded)) { $messages = $decoded; } } // Build available tools from whitelisted routes (strip _meta) $tools_with_meta = $this->security->get_enabled_tools(); $tools = array_map(function($tool) { unset($tool['_meta']); return $tool; }, $tools_with_meta); // Build payload - messages already contain tool_result blocks when needed $payload = array( 'messages' => $messages, 'tools_schema' => $tools, ); // Call external API $api_response = $this->call_external_api($jwt_token, $payload); if ($api_response['success']) { wp_send_json_success($api_response['data']); } else { wp_send_json_error($api_response['error'], $api_response['status'] ?? 500); } } /** * Handle tool execution request * Executes a single tool and returns the result */ public function handle_execute_tool() { // Verify nonce if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) { wp_send_json_error(array('message' => 'Security check failed'), 403); } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions'), 403); } // Get tool info $tool_use_id = isset($_POST['tool_use_id']) ? sanitize_text_field(wp_unslash($_POST['tool_use_id'])) : ''; $tool_name = isset($_POST['tool_name']) ? sanitize_text_field(wp_unslash($_POST['tool_name'])) : ''; $tool_params = array(); if (isset($_POST['tool_params']) && is_string($_POST['tool_params'])) { $decoded = json_decode(wp_unslash($_POST['tool_params']), true); if (is_array($decoded)) { $tool_params = $decoded; } } if (empty($tool_name)) { wp_send_json_error(array('message' => 'Tool name is required'), 400); } // Execute the tool $tool_call = array( 'id' => $tool_use_id, 'name' => $tool_name, 'input' => $tool_params, ); $executed = $this->execute_tool_calls(array($tool_call)); // Get result content $result_content = ''; if (!empty($executed) && isset($executed[0]['result'])) { $result = $executed[0]['result']; $result_content = is_string($result) ? $result : wp_json_encode($result); } wp_send_json_success(array( 'tool_use_id' => $tool_use_id, 'tool_name' => $tool_name, 'content' => $result_content, 'execution_log' => $executed, )); } /** * Handle chat request from frontend */ public function handle_chat_request() { // Verify nonce if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) { wp_send_json_error(array('message' => 'Security check failed'), 403); } // Check permissions if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions'), 403); } // Get user message $message = isset($_POST['message']) ? sanitize_textarea_field(wp_unslash($_POST['message'])) : ''; if (empty($message)) { wp_send_json_error(array('message' => 'Message is required'), 400); } // Get conversation history (optional) $history = array(); if (isset($_POST['history']) && is_string($_POST['history'])) { $decoded = json_decode(wp_unslash($_POST['history']), true); if (is_array($decoded)) { $history = $decoded; } } // Execute chat $result = $this->process_chat($message, $history); if ($result['success']) { wp_send_json_success($result['data']); } else { wp_send_json_error($result['error'], $result['status'] ?? 500); } } /** * Process chat message and execute tool loop * * @param string $message User message * @param array $history Conversation history * @return array */ public function process_chat($message, array $history = array()) { // Get JWT token $jwt_token = get_option('aibui_jwt_token', ''); if (empty($jwt_token)) { return array( 'success' => false, 'error' => array('message' => 'Authentication required. Please sign in first.'), 'status' => 401, ); } // Build available tools from whitelisted routes $tools_with_meta = $this->security->get_enabled_tools(); // Strip _meta from tools before sending to API (Claude doesn't accept extra fields) $tools = array_map(function($tool) { unset($tool['_meta']); return $tool; }, $tools_with_meta); // Prepare system context $system_context = $this->build_system_context(); // Initialize loop variables $iterations = 0; $final_response = null; $final_credits = null; $pending_tool_results = array(); // Tool results to send in next request // Build initial messages array from history $messages = array(); foreach ($history as $hist_item) { if (isset($hist_item['role']) && isset($hist_item['content'])) { $messages[] = array( 'role' => $hist_item['role'], 'content' => $hist_item['content'], ); } } // Add current user message if (!empty($message)) { $messages[] = array( 'role' => 'user', 'content' => $message, ); } // Main agent loop while ($iterations < self::MAX_LOOP_ITERATIONS) { $iterations++; // Build API request payload $payload = array( 'messages' => $messages, 'tools_schema' => $tools, ); // Add tool_results if we have results from previous iteration if (!empty($pending_tool_results)) { $payload['tool_results'] = $pending_tool_results; $pending_tool_results = array(); // Clear after adding to payload } // Call external API $api_response = $this->call_external_api($jwt_token, $payload); if (!$api_response['success']) { return $api_response; } $response_data = $api_response['data']; $credits_left = $response_data['creditsLeft'] ?? $api_response['credits'] ?? null; // Check if AI wants to use tools (type === 'tool_request') if (isset($response_data['type']) && $response_data['type'] === 'tool_request') { // 1. Store assistant_message in messages for proper conversation flow if (isset($response_data['assistant_message'])) { $messages[] = array( 'role' => 'assistant', 'content' => $response_data['assistant_message'], ); } // 2. Get tools to execute - can be single tool or array $tools_to_execute = array(); // New format: tools array if (isset($response_data['tools']) && is_array($response_data['tools'])) { $tools_to_execute = $response_data['tools']; } // Legacy format: single tool elseif (isset($response_data['tool_use_id'])) { $tools_to_execute[] = array( 'tool_use_id' => $response_data['tool_use_id'], 'tool_name' => $response_data['tool_name'] ?? '', 'tool_params' => $response_data['tool_params'] ?? array(), ); } // 3. Execute ALL tools and collect results with REAL tool_use_id foreach ($tools_to_execute as $tool_request) { $tool_use_id = $tool_request['tool_use_id'] ?? ''; $tool_name = $tool_request['tool_name'] ?? ''; $tool_params = $tool_request['tool_params'] ?? array(); // Execute the tool $tool_call = array( 'id' => $tool_use_id, 'name' => $tool_name, 'input' => $tool_params, ); $executed = $this->execute_tool_calls(array($tool_call)); // Get result content $result_content = ''; if (!empty($executed) && isset($executed[0]['result'])) { $result = $executed[0]['result']; $result_content = is_string($result) ? $result : wp_json_encode($result); } // Add to pending results with the REAL tool_use_id from Claude $pending_tool_results[] = array( 'tool_use_id' => $tool_use_id, 'content' => $result_content, ); } // Continue loop to send results back continue; } // No more tool calls, we have the final response (type === 'text') $final_response = $response_data; $final_credits = $credits_left; break; } if ($iterations >= self::MAX_LOOP_ITERATIONS) { return array( 'success' => false, 'error' => array('message' => 'Maximum tool execution iterations reached'), 'status' => 500, ); } return array( 'success' => true, 'data' => array( 'response' => $final_response['content'] ?? '', 'tool_executions' => $this->executor->get_execution_log(), 'iterations' => $iterations, 'credits' => $final_credits ?? null, ), ); } /** * Call the external Node.js API * * @param string $jwt_token * @param array $payload * @return array */ private function call_external_api($jwt_token, array $payload) { $url = self::API_BASE_URL . self::AGENT_ENDPOINT; $response = wp_remote_post($url, array( 'timeout' => self::REQUEST_TIMEOUT, 'headers' => array( 'Authorization' => 'Bearer ' . $jwt_token, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode($payload), )); if (is_wp_error($response)) { return array( 'success' => false, 'error' => array('message' => 'API request failed: ' . $response->get_error_message()), 'status' => 500, ); } $status_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); // Extract remaining credits if provided by API (check creditsLeft first - Node.js API format) $credits = null; if (is_array($data)) { if (isset($data['creditsLeft'])) { $credits = (int) $data['creditsLeft']; } elseif (isset($data['credits'])) { $credits = (int) $data['credits']; } elseif (isset($data['remaining_credits'])) { $credits = (int) $data['remaining_credits']; } elseif (isset($data['meta']['credits'])) { $credits = (int) $data['meta']['credits']; } } // Handle errors, including insufficient credits (402) if ($status_code >= 400) { // Log the raw response for debugging $error_message = isset($data['message']) ? $data['message'] : 'API error'; // Include more details in error for debugging if (empty($data['message']) && !empty($body)) { $error_message = 'API error'; // $error_message = 'API error: ' . substr($body, 0, 500); } // Special handling for 402 Payment Required (not enough credits) if ($status_code === 402) { $credits_url = admin_url('admin.php?page=aibui-credits'); $error_message = __('You do not have enough AI credits to run this request. Please top up your credits on the Credits page.', 'ai-builder'); return array( 'success' => false, 'error' => array( 'message' => $error_message, 'code' => $status_code, 'credits' => $credits, 'credits_url' => $credits_url, ), 'status' => $status_code, 'credits' => $credits, ); } return array( 'success' => false, 'error' => array( 'message' => $error_message, 'code' => $status_code, 'credits' => $credits, ), 'status' => $status_code, 'credits' => $credits, ); } return array( 'success' => true, 'data' => $data, 'credits' => $credits, ); } /** * Execute tool calls requested by AI * * @param array $tool_calls * @return array Results */ private function execute_tool_calls(array $tool_calls) { $results = array(); foreach ($tool_calls as $call) { $tool_name = isset($call['name']) ? $call['name'] : ''; $tool_id = isset($call['id']) ? $call['id'] : uniqid('tool_'); $input = isset($call['input']) ? $call['input'] : array(); // Parse input if it's a string if (is_string($input)) { $decoded = json_decode($input, true); $input = is_array($decoded) ? $decoded : array(); } // Execute the tool $result = $this->executor->execute_tool($tool_name, $input); $results[] = array( 'tool_use_id' => $tool_id, 'tool_name' => $tool_name, 'result' => $result, ); } return $results; } /** * Build system context for the AI * * @return string */ private function build_system_context() { $site_name = get_bloginfo('name'); $site_url = get_site_url(); $wp_version = get_bloginfo('version'); $current_user = wp_get_current_user(); $context = "You are an AI assistant helping to manage a WordPress website.\n\n"; $context .= "Site Information:\n"; $context .= "- Site Name: {$site_name}\n"; $context .= "- Site URL: {$site_url}\n"; $context .= "- WordPress Version: {$wp_version}\n"; $context .= "- Current User: {$current_user->display_name} ({$current_user->user_email})\n\n"; $context .= "You have access to WordPress REST API tools to help manage content.\n"; $context .= "Always verify actions with the user before making changes.\n"; $context .= "When listing items, limit results to be concise.\n"; $context .= "Provide helpful summaries of data retrieved.\n"; return $context; } /** * Get site information * * @return array */ private function get_site_info() { return array( 'name' => get_bloginfo('name'), 'url' => get_site_url(), 'admin_url' => admin_url(), 'wp_version' => get_bloginfo('version'), 'language' => get_bloginfo('language'), 'timezone' => wp_timezone_string(), ); } /** * Handle get routes request (for settings page) */ public function handle_get_routes() { if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) { wp_send_json_error(array('message' => 'Security check failed'), 403); } if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions'), 403); } $routes = $this->security->get_routes_with_status(); wp_send_json_success($routes); } /** * Handle update routes request */ public function handle_update_routes() { if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) { wp_send_json_error(array('message' => 'Security check failed'), 403); } if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions'), 403); } $enabled_routes = array(); if (isset($_POST['enabled_routes']) && is_string($_POST['enabled_routes'])) { $decoded = json_decode(wp_unslash($_POST['enabled_routes']), true); if (is_array($decoded)) { $enabled_routes = array_map('sanitize_text_field', $decoded); } } $success = $this->security->update_whitelist($enabled_routes); if ($success) { wp_send_json_success(array('message' => 'Routes updated successfully')); } else { wp_send_json_error(array('message' => 'Failed to update routes'), 500); } } /** * Handle get stats request */ public function handle_get_stats() { if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) { wp_send_json_error(array('message' => 'Security check failed'), 403); } if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions'), 403); } $stats = $this->security->get_stats(); wp_send_json_success($stats); } /** * Handle reset routes request */ public function handle_reset_routes() { if (!check_ajax_referer('aibui_agent_nonce', 'nonce', false)) { wp_send_json_error(array('message' => 'Security check failed'), 403); } if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions'), 403); } $success = $this->security->reset_to_defaults(); if ($success) { wp_send_json_success(array('message' => 'Routes reset to defaults')); } else { wp_send_json_error(array('message' => 'Failed to reset routes'), 500); } } /** * Get the security service instance * * @return AIBUI_Agent_Security_Service */ public function get_security_service() { return $this->security; } /** * Get the discovery service instance * * @return AIBUI_Agent_Discovery_Service */ public function get_discovery_service() { return $this->discovery; } /** * Get the execution service instance * * @return AIBUI_Agent_Execution_Service */ public function get_execution_service() { return $this->executor; } }