strtoupper( $method ), 'timeout' => 30, 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', ), ); if ( $auth ) { $cookie = self::get_saved_cookie(); if ( $cookie ) { $args['headers']['Cookie'] = $cookie; } } if ( null !== $body ) { $args['body'] = wp_json_encode( $body ); } return $args; } /** * Execute an HTTP request and return the decoded response data. * * On a 401 the method automatically attempts a token refresh and retries * the original request once. If the refresh also fails, auth state is * cleared and a WP_Error is returned. * * @param string $method HTTP method. * @param string $endpoint API endpoint path (e.g. '/v1/projects'). * @param array|null $body JSON request body. * @param bool $auth Whether this is an authenticated request. * @param bool $is_retry Internal flag – prevents infinite refresh loop. * @return array|WP_Error Decoded `data` field on success, WP_Error on failure. */ public static function request( $method, $endpoint, $body = null, $auth = true, $is_retry = false ) { $url = rtrim( PLUGIN_API_BASE_URL, '/' ) . '/' . ltrim( $endpoint, '/' ); $args = self::build_args( $method, $body, $auth ); $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $status_code = wp_remote_retrieve_response_code( $response ); $response_body = wp_remote_retrieve_body( $response ); $decoded = json_decode( $response_body, true ); // Handle 401 – attempt refresh then retry (once). if ( 401 === $status_code && ! $is_retry && $auth ) { $refresh_result = self::refresh_token(); if ( is_wp_error( $refresh_result ) ) { self::clear_auth(); return new WP_Error( 'auth_expired', __( 'Your session has expired. Please log in again.', AI_CHATBOT_DOMAIN ) ); } // Retry the original request with the new cookie. return self::request( $method, $endpoint, $body, $auth, true ); } // Surface API-level errors. Prefer the specific error.detail (e.g. // "Widget title must be between 1 and 80 characters.") over the // generic top-level message ("Validation failed...") since it's the // only part that actually tells the user what to fix. if ( $status_code < 200 || $status_code >= 300 ) { $message = isset( $decoded['error']['detail'] ) ? $decoded['error']['detail'] : ( isset( $decoded['message'] ) ? $decoded['message'] : __( 'An unknown error occurred.', AI_CHATBOT_DOMAIN ) ); $code = isset( $decoded['error']['code'] ) ? $decoded['error']['code'] : 'api_error'; return new WP_Error( $code, $message ); } // Save any new cookies returned by the server. self::save_cookies_from_response( $response ); return isset( $decoded['data'] ) ? $decoded['data'] : $decoded; } // ----------------------------------------------------------------------- // Authentication APIs (public – no cookie required) // ----------------------------------------------------------------------- /** * Register a new account. * * @param string $email User email. * @param string $password User password. * @param string $first_name * @param string $last_name (optional) * @param string $org_name Organisation / company name. * @return array|WP_Error */ public static function signup( $email, $password, $first_name, $last_name, $org_name ) { $body = array( 'email' => $email, 'password' => $password, 'firstName' => $first_name, 'lastName' => $last_name, 'organizationName' => $org_name, 'region' => 'eu-west-1', 'source' => 'ai-agent', ); $result = self::request( 'POST', '/v1/auth/register', $body, false ); if ( ! is_wp_error( $result ) ) { // Cookies are saved inside request() via save_cookies_from_response(). if ( isset( $result['organization_id'] ) ) { update_option( self::opt_org_id(), sanitize_text_field( $result['organization_id'] ) ); } if ( isset( $result['account_id'] ) ) { update_option( self::opt_account_id(), sanitize_text_field( $result['account_id'] ) ); } } return $result; } /** * Log in with email and password. * * @param string $email * @param string $password * @return array|WP_Error */ public static function login( $email, $password ) { $body = array( 'email' => $email, 'password' => $password ); $result = self::request( 'POST', '/v1/auth/login', $body, false ); if ( ! is_wp_error( $result ) ) { if ( isset( $result['organization_id'] ) ) { update_option( self::opt_org_id(), sanitize_text_field( $result['organization_id'] ) ); } if ( isset( $result['account_id'] ) ) { update_option( self::opt_account_id(), sanitize_text_field( $result['account_id'] ) ); } } return $result; } /** * Initiate OTP login – sends OTP to the user's email. * * @param string $email * @return array|WP_Error Returns `data` which contains `session` string. */ public static function login_otp_init( $email ) { return self::request( 'POST', '/v1/auth/login_otp', array( 'email' => $email, 'source' => 'ai-agent' ), false ); } /** * Verify OTP and complete login. * * @param string $email * @param string $otp * @param string $session Session token from login_otp_init(). * @return array|WP_Error */ public static function verify_otp( $email, $otp, $session ) { $body = array( 'email' => $email, 'otp' => $otp, 'session' => $session ); $result = self::request( 'POST', '/v1/auth/verify_otp', $body, false ); if ( ! is_wp_error( $result ) ) { if ( isset( $result['organization_id'] ) ) { update_option( self::opt_org_id(), sanitize_text_field( $result['organization_id'] ) ); } if ( isset( $result['account_id'] ) ) { update_option( self::opt_account_id(), sanitize_text_field( $result['account_id'] ) ); } } return $result; } /** * Refresh the access token using the stored refresh-token cookie. * * Called automatically by request() on 401; also available directly. * * @return array|WP_Error */ public static function refresh_token() { // We send no body but DO send the cookie (which contains the refresh token). $url = rtrim( PLUGIN_API_BASE_URL, '/' ) . '/v1/auth/refresh'; $args = self::build_args( 'POST', null, true ); $response = wp_remote_request( $url, $args ); if ( is_wp_error( $response ) ) { return $response; } $status_code = wp_remote_retrieve_response_code( $response ); if ( $status_code < 200 || $status_code >= 300 ) { return new WP_Error( 'refresh_failed', __( 'Token refresh failed.', AI_CHATBOT_DOMAIN ) ); } self::save_cookies_from_response( $response ); return true; } // ----------------------------------------------------------------------- // Project APIs (authenticated) // ----------------------------------------------------------------------- /** * Retrieve all projects for the current user. * * @return array|WP_Error Array of project objects. */ public static function get_projects() { return self::request( 'GET', '/v1/projects' ); } /** * Create a new project with an auto-generated name. * * @return array|WP_Error Created project object. */ public static function create_project() { $body = array( 'project_name' => 'wp_' . time() ); $result = self::request( 'POST', '/v1/projects', $body ); if ( ! is_wp_error( $result ) && isset( $result['project_id'] ) ) { update_option( self::opt_project_id(), sanitize_text_field( $result['project_id'] ) ); } return $result; } // ----------------------------------------------------------------------- // AI Agent APIs (authenticated) // ----------------------------------------------------------------------- /** * Get all AI agents for a project. * * @param string $project_id * @return array|WP_Error */ public static function get_agents( $project_id ) { return self::request( 'GET', '/v1/ai-agent/chatbot/' . rawurlencode( $project_id ) ); } /** * Create a new AI agent. * * @param string $project_id * @param string $name * @param string $language Language code (e.g. 'en'). * @param string $avatar_url Optional avatar URL. * @return array|WP_Error */ public static function create_agent( $project_id, $name, $language, $avatar_url = '' ) { $body = array( 'name' => $name, 'description' => 'You are a helpful AI Agent', 'type' => 2, 'access_type' => 0, 'llm_source' => 1, 'language' => $language, ); if ( ! empty( $avatar_url ) ) { $body['avatar_url'] = $avatar_url; } return self::request( 'POST', '/v1/ai-agent/chatbot/' . rawurlencode( $project_id ), $body ); } /** * Update an existing AI agent. * * @param string $project_id * @param string $chatbot_id * @param string $name * @param string $description * @param string $avatar_url * @return array|WP_Error */ public static function update_agent( $project_id, $chatbot_id, $name, $description, $avatar_url = '' ) { $body = array( 'name' => $name, 'description' => $description, 'avatar_url' => $avatar_url, ); return self::request( 'PUT', '/v1/ai-agent/chatbot/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ), $body ); } /** * Delete an AI agent. * * @param string $project_id * @param string $chatbot_id * @return array|WP_Error */ public static function delete_agent( $project_id, $chatbot_id ) { return self::request( 'DELETE', '/v1/ai-agent/chatbot/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ) ); } // ----------------------------------------------------------------------- // Phase 2 APIs (authenticated) // ----------------------------------------------------------------------- /** Get appearance settings for an agent. */ public static function get_appearance_settings( $project_id, $chatbot_id ) { return self::request( 'GET', '/v1/ai-agent/chatbot_appearance_settings/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ) ); } /** Update appearance settings for an agent. */ public static function update_appearance_settings( $project_id, $chatbot_id, $data ) { return self::request( 'PUT', '/v1/ai-agent/chatbot_appearance_settings/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ), $data ); } /** Get AI settings for an agent. */ public static function get_ai_settings( $project_id, $chatbot_id ) { return self::request( 'GET', '/v1/ai-agent/chatbot_ai_settings/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ) ); } /** Update AI settings for an agent. */ public static function update_ai_settings( $project_id, $chatbot_id, $data ) { return self::request( 'PUT', '/v1/ai-agent/chatbot_ai_settings/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ), $data ); } /** Get linked knowledge-base content for an agent (paginated). */ public static function get_linked_content( $project_id, $chatbot_id, $page_number = 1, $page_size = 10, $asset_type = 'WebsiteURL' ) { $endpoint = add_query_arg( array( 'pageNumber' => (int) $page_number, 'pageSize' => (int) $page_size, 'assetType' => $asset_type ), rtrim( PLUGIN_API_BASE_URL, '/' ) . '/v1/ai-agent/chatbot_asset_link/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ) ); $args = self::build_args( 'GET', null, true ); $response = wp_remote_request( $endpoint, $args ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); if ( 401 === $code ) { $r = self::refresh_token(); if ( is_wp_error( $r ) ) { self::clear_auth(); return new WP_Error( 'auth_expired', __( 'Session expired.', AI_CHATBOT_DOMAIN ) ); } $args['headers']['Cookie'] = self::get_saved_cookie(); $response = wp_remote_request( $endpoint, $args ); $code = wp_remote_retrieve_response_code( $response ); } $decoded = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code < 200 || $code >= 300 ) { return new WP_Error( 'api_error', isset( $decoded['message'] ) ? $decoded['message'] : __( 'Failed to load knowledge base.', AI_CHATBOT_DOMAIN ) ); } self::save_cookies_from_response( $response ); return isset( $decoded['data'] ) ? $decoded['data'] : $decoded; } /** Link content to an agent (add to knowledge base). */ public static function link_content( $project_id, $chatbot_id, $content, $file_name ) { return self::request( 'POST', '/v1/ai-agent/link_content/' . rawurlencode( $project_id ), array( 'chatbot_id' => $chatbot_id, 'content' => $content, 'file_name' => $file_name, 'is_webpage' => true, 'source_platform' => 'wordpress', ) ); } /** * Crawl a website and feed it into an agent's knowledge base. * * @param string $project_id * @param string $chatbot_id * @param string $url The start URL to crawl. * @return array|WP_Error */ public static function crawl_website( $project_id, $chatbot_id, $url ) { return self::request( 'POST', '/v1/ai-agent/crawl_website', array( 'project_id' => $project_id, 'chatbot_id' => $chatbot_id, 'start_urls' => array( array( 'url' => $url ) ), 'max_crawl_pages' => 1, 'max_crawl_depth' => 1, 'use_sitemap' => '0', 'is_main_website' => true, ) ); } /** Delink content from an agent's knowledge base. */ public static function delink_content( $linkage_id, $chatbot_id, $asset_id, $asset_type = '' ) { $body = array( 'linkage_id' => (int) $linkage_id, 'chatbot_id' => $chatbot_id, 'asset_id' => (int) $asset_id ); if ( ! empty( $asset_type ) ) { $body['asset_type'] = $asset_type; } return self::request( 'DELETE', '/v1/ai-agent/chatbot_asset_delink', $body ); } // ----------------------------------------------------------------------- // Agent statistics // ----------------------------------------------------------------------- /** * Get message stats for an agent. * * @param string $chatbot_id * @param string $start_datetime Format: 'yyyy-MM-dd HH:mm'. * @param string $end_datetime Format: 'yyyy-MM-dd HH:mm'. * @param int $time_offset Minutes offset from UTC. * @return array|WP_Error */ public static function get_agent_stats( $chatbot_id, $start_datetime, $end_datetime, $time_offset = 0 ) { $endpoint = add_query_arg( array( 'chatbotId' => rawurlencode( $chatbot_id ), 'startDateTime' => rawurlencode( $start_datetime ), 'endDateTime' => rawurlencode( $end_datetime ), 'timeOffset' => (int) $time_offset, ), rtrim( PLUGIN_API_BASE_URL, '/' ) . '/v1/ai-agent/message_stats' ); // We call wp_remote_request directly here because the full URL is already built. $args = self::build_args( 'GET', null, true ); $response = wp_remote_request( $endpoint, $args ); if ( is_wp_error( $response ) ) { return $response; } $status_code = wp_remote_retrieve_response_code( $response ); // Handle 401 with auto-refresh (once). if ( 401 === $status_code ) { $refresh = self::refresh_token(); if ( is_wp_error( $refresh ) ) { self::clear_auth(); return new WP_Error( 'auth_expired', __( 'Session expired. Please log in again.', AI_CHATBOT_DOMAIN ) ); } // Retry. $args['headers']['Cookie'] = self::get_saved_cookie(); $response = wp_remote_request( $endpoint, $args ); $status_code = wp_remote_retrieve_response_code( $response ); } $decoded = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $status_code < 200 || $status_code >= 300 ) { $message = isset( $decoded['message'] ) ? $decoded['message'] : __( 'Stats request failed.', AI_CHATBOT_DOMAIN ); return new WP_Error( 'stats_error', $message ); } self::save_cookies_from_response( $response ); return isset( $decoded['data'] ) ? $decoded['data'] : $decoded; } /** * Get inbox sessions. */ public static function get_inbox_sessions( $chatbot_id, $next_page_token = '' ) { $endpoint = rtrim( PLUGIN_API_BASE_URL, '/' ) . '/v1/ai-agent/inbox/sessions/' . rawurlencode( $chatbot_id ); if ( ! empty( $next_page_token ) ) { $endpoint = add_query_arg( 'next_page_token', rawurlencode( $next_page_token ), $endpoint ); } $args = self::build_args( 'GET', null, true ); $response = wp_remote_request( $endpoint, $args ); if ( is_wp_error( $response ) ) { return $response; } $status_code = wp_remote_retrieve_response_code( $response ); if ( 401 === $status_code ) { $refresh = self::refresh_token(); if ( is_wp_error( $refresh ) ) { self::clear_auth(); return new WP_Error( 'auth_expired', __( 'Session expired. Please log in again.', AI_CHATBOT_DOMAIN ) ); } $args['headers']['Cookie'] = self::get_saved_cookie(); $response = wp_remote_request( $endpoint, $args ); $status_code = wp_remote_retrieve_response_code( $response ); } $decoded = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $status_code < 200 || $status_code >= 300 ) { $message = isset( $decoded['message'] ) ? $decoded['message'] : __( 'Failed to load sessions.', AI_CHATBOT_DOMAIN ); return new WP_Error( 'api_error', $message ); } self::save_cookies_from_response( $response ); return isset( $decoded['data'] ) ? $decoded['data'] : $decoded; } /** * Get inbox chats for a session. */ public static function get_inbox_chats( $chatbot_id, $session_id ) { $endpoint = rtrim( PLUGIN_API_BASE_URL, '/' ) . '/v1/ai-agent/inbox/chats/' . rawurlencode( $chatbot_id ) . '/' . rawurlencode( $session_id ); $args = self::build_args( 'GET', null, true ); $response = wp_remote_request( $endpoint, $args ); if ( is_wp_error( $response ) ) { return $response; } $status_code = wp_remote_retrieve_response_code( $response ); if ( 401 === $status_code ) { $refresh = self::refresh_token(); if ( is_wp_error( $refresh ) ) { self::clear_auth(); return new WP_Error( 'auth_expired', __( 'Session expired. Please log in again.', AI_CHATBOT_DOMAIN ) ); } $args['headers']['Cookie'] = self::get_saved_cookie(); $response = wp_remote_request( $endpoint, $args ); $status_code = wp_remote_retrieve_response_code( $response ); } $decoded = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $status_code < 200 || $status_code >= 300 ) { $message = isset( $decoded['message'] ) ? $decoded['message'] : __( 'Failed to load chats.', AI_CHATBOT_DOMAIN ); return new WP_Error( 'api_error', $message ); } self::save_cookies_from_response( $response ); return isset( $decoded['data'] ) ? $decoded['data'] : $decoded; } /** * Get organization account limits. */ public static function get_account_limits() { return self::request( 'GET', '/v1/account/limit' ); } // ----------------------------------------------------------------------- // Tool Credentials (WooCommerce and other integrations) // ----------------------------------------------------------------------- /** Get all tool credentials for a project. */ public static function get_tool_credentials( $project_id ) { return self::request( 'GET', '/v1/ai-agent/tools/credential/' . rawurlencode( $project_id ) ); } /** Get all tool linkages for a specific agent. */ public static function get_tool_linkages( $project_id, $chatbot_id ) { return self::request( 'GET', '/v1/ai-agent/tools/link/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ) ); } /** Link a tool credential to an agent. Returns array with linkage_id on success. */ public static function link_tool_credential( $project_id, $chatbot_id, $integration_id, $credential_id ) { return self::request( 'POST', '/v1/ai-agent/tools/link', array( 'project_id' => $project_id, 'chatbot_id' => $chatbot_id, 'integration_id' => (int) $integration_id, 'credential_id' => $credential_id, 'is_active' => true, ) ); } /** Remove a tool linkage from an agent. */ public static function unlink_tool_credential( $project_id, $chatbot_id, $linkage_id ) { return self::request( 'DELETE', '/v1/ai-agent/tools/link/' . rawurlencode( $project_id ) . '/' . rawurlencode( $chatbot_id ) . '/' . rawurlencode( $linkage_id ) ); } /** Create a new tool credential. */ public static function create_tool_credential( $project_id, $integration_id, $auth_type, $auth_data_json, $connection_name ) { return self::request( 'POST', '/v1/ai-agent/tools/credential', array( 'project_id' => $project_id, 'integration_id' => (int) $integration_id, 'auth_type' => $auth_type, 'auth_data_json' => $auth_data_json, 'connection_name' => $connection_name, ) ); } /** Delete a tool credential. */ public static function delete_tool_credential( $project_id, $credential_id ) { return self::request( 'DELETE', '/v1/ai-agent/tools/credential/' . rawurlencode( $project_id ) . '/' . rawurlencode( $credential_id ) ); } }