# ai-builder/2.7.8/includes/class-agent-chat-handler.php

AI Builder – Generate pages, blocks, images &amp; translate with AI, version 2.7.8. 692 lines.

- Page: https://pluginprobe.com/plugins/ai-builder/2.7.8/code/includes/class-agent-chat-handler.php
- Raw: https://pluginprobe.com/plugins/ai-builder/2.7.8/raw/includes/class-agent-chat-handler.php
- Modified: 2025-12-18T11:42:50+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/ai-builder/2.7.8/code/includes/class-agent-chat-handler.php#L10-L20`.

```php
<?php
/**
 * Agent Chat Handler
 * 
 * Main chat handler that acts as a proxy between the frontend and
 * the external Node.js API. Manages tool execution loop.
 *
 * @package AI_Builder
 */

if (!defined('ABSPATH')) {
    exit;
}

class AIBUI_Agent_Chat_Handler
{
    /**
     * External API base URL
     */
    const API_BASE_URL = 'https://api.wordpress-ai-builder.com/api';
    // const API_BASE_URL = 'http://localhost:8080/api';

    /**
     * Agent chat endpoint
     */
    const AGENT_ENDPOINT = '/agent/chat';

    /**
     * Maximum iterations in tool execution loop
     */
    const MAX_LOOP_ITERATIONS = 6;

    /**
     * Request timeout in seconds
     */
    const REQUEST_TIMEOUT = 120;

    /**
     * Discovery service
     *
     * @var AIBUI_Agent_Discovery_Service
     */
    private $discovery;

    /**
     * Security service
     *
     * @var AIBUI_Agent_Security_Service
     */
    private $security;

    /**
     * Execution service
     *
     * @var AIBUI_Agent_Execution_Service
     */
    private $executor;

    /**
     * Conversation history
     *
     * @var array
     */
    private $conversation_history = array();

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->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;
    }
}


```
