__( 'AI Chatbot, Agents & MCP for Amazon Bedrock configuration', 'ai-chat-for-amazon-bedrock' ), 'test' => array( $this, 'site_health_configuration' ), ); return $tests; } /** * Site Health result for plugin configuration. * * @return array */ public function site_health_configuration() { $checks = $this->run( false ); $failed = array(); $warnings = array(); foreach ( $checks as $check ) { if ( 'fail' === $check['status'] ) { $failed[] = $check; } elseif ( 'warn' === $check['status'] ) { $warnings[] = $check; } } $result = array( 'label' => __( 'Amazon Bedrock chat is configured correctly', 'ai-chat-for-amazon-bedrock' ), 'status' => 'good', 'badge' => array( 'label' => __( 'AI Chat Bedrock', 'ai-chat-for-amazon-bedrock' ), 'color' => 'blue', ), 'description' => '

' . esc_html__( 'Credentials, region, model access, and chat security defaults were verified.', 'ai-chat-for-amazon-bedrock' ) . '

', 'actions' => '', 'test' => 'ai_chat_bedrock_configuration', ); $problems = array_merge( $failed, $warnings ); if ( ! empty( $problems ) ) { $result['status'] = empty( $failed ) ? 'recommended' : 'critical'; $result['label'] = empty( $failed ) ? __( 'Amazon Bedrock chat has configuration recommendations', 'ai-chat-for-amazon-bedrock' ) : __( 'Amazon Bedrock chat is not fully configured', 'ai-chat-for-amazon-bedrock' ); $items = ''; foreach ( $problems as $problem ) { $items .= '
  • ' . esc_html( $problem['label'] ) . ': ' . esc_html( $problem['message'] ) . '
  • '; } $result['description'] = ''; $result['actions'] = sprintf( '

    %s

    ', esc_url( admin_url( 'admin.php?page=ai-chat-for-amazon-bedrock-diagnostics' ) ), esc_html__( 'Open Bedrock diagnostics', 'ai-chat-for-amazon-bedrock' ) ); } return $result; } /** * Run configuration checks. * * @param bool $include_live Whether to perform a live Bedrock invocation. * @return array */ public function run( $include_live = false ) { $options = get_option( 'ai_chat_bedrock_settings', array() ); $options = is_array( $options ) ? $options : array(); $checks = array(); // Asked for a fresh look, as after attaching an IAM role, nothing remembered stands in for it. if ( $include_live && class_exists( 'AI_Chat_Bedrock_AWS_Credentials' ) ) { AI_Chat_Bedrock_AWS_Credentials::flush_cache(); } $checks[] = $this->check_php(); $checks[] = $this->check_credentials( $options ); $checks[] = $this->check_region( $options ); $checks[] = $this->check_model( $options ); $checks[] = $this->check_streaming( $options ); $checks[] = $this->check_encryption(); $checks[] = $this->check_public_access( $options ); $checks[] = $this->check_mcp(); if ( class_exists( 'AI_Chat_Bedrock_Ontology' ) ) { $checks[] = $this->check_agents(); } $checks[] = $this->check_schedules(); $checks[] = $this->check_guardrail( $options ); $checks[] = $this->check_knowledge_base( $options ); $checks[] = $this->check_vector_store( $options ); if ( $include_live ) { $checks[] = $this->check_live_invocation( $options ); } return $checks; } private function check_php() { $ok = version_compare( PHP_VERSION, '7.4', '>=' ); return $this->result( 'php', __( 'PHP version', 'ai-chat-for-amazon-bedrock' ), $ok ? 'pass' : 'fail', $ok /* translators: %s: PHP version. */ ? sprintf( __( 'PHP %s meets the plugin requirement.', 'ai-chat-for-amazon-bedrock' ), PHP_VERSION ) : __( 'PHP 7.4 or newer is required.', 'ai-chat-for-amazon-bedrock' ) ); } private function check_credentials( $options ) { $status = AI_Chat_Bedrock_AWS_Credentials::describe( $options ); if ( ! $status['configured'] ) { return $this->result( 'credentials', __( 'AWS credentials', 'ai-chat-for-amazon-bedrock' ), 'fail', __( 'No usable AWS credentials were found. Paste an Amazon Bedrock API key, add access keys, or enable IAM role credentials.', 'ai-chat-for-amazon-bedrock' ) ); } $state = in_array( $status['source'], array( 'options' ), true ) ? 'warn' : 'pass'; $message = $status['message']; if ( 'options' === $status['source'] ) { $message = AI_Chat_Bedrock_Translation::sentences( $message, __( 'Consider wp-config.php constants or an IAM role so keys are not stored in the database.', 'ai-chat-for-amazon-bedrock' ) ); } if ( 0 === strpos( $status['source'], 'api_key_' ) ) { if ( ! empty( $status['temporary'] ) ) { $state = 'warn'; $message = AI_Chat_Bedrock_Translation::sentences( $message, __( 'This is a short-term key, which stops working within 12 hours. Use a long-term key or an IAM role.', 'ai-chat-for-amazon-bedrock' ) ); } // Agents endpoints refuse API keys, so a feature that uses them needs signing credentials too. $needs_signing = ! empty( $options['knowledge_base_id'] ) || ! empty( $options['prompt_id'] ); if ( $needs_signing && ! ( new AI_Chat_Bedrock_AWS() )->has_signing_credentials() ) { $state = 'warn'; $message = AI_Chat_Bedrock_Translation::sentences( $message, __( 'A Knowledge Base or managed prompt is configured, and those only accept signed requests. Add access keys or an IAM role for them; chat keeps using the API key.', 'ai-chat-for-amazon-bedrock' ) ); } } return $this->result( 'credentials', __( 'AWS credentials', 'ai-chat-for-amazon-bedrock' ), $state, $message ); } private function check_region( $options ) { $region = isset( $options['aws_region'] ) ? sanitize_key( $options['aws_region'] ) : ''; $regions = AI_Chat_Bedrock_Models::regions(); if ( '' === $region || ! isset( $regions[ $region ] ) ) { return $this->result( 'region', __( 'AWS region', 'ai-chat-for-amazon-bedrock' ), 'fail', __( 'Select a supported AWS region for Amazon Bedrock.', 'ai-chat-for-amazon-bedrock' ) ); } return $this->result( 'region', __( 'AWS region', 'ai-chat-for-amazon-bedrock' ), 'pass', $regions[ $region ] . ' (' . $region . ')' ); } private function check_model( $options ) { $model = isset( $options['model_id'] ) ? (string) $options['model_id'] : ''; if ( ! AI_Chat_Bedrock_Models::is_valid_id( $model ) ) { return $this->result( 'model', __( 'Model selection', 'ai-chat-for-amazon-bedrock' ), 'fail', __( 'Select a Bedrock model in the settings.', 'ai-chat-for-amazon-bedrock' ) ); } $catalog = AI_Chat_Bedrock_Models::options(); if ( ! isset( $catalog[ $model ] ) ) { return $this->result( 'model', __( 'Model selection', 'ai-chat-for-amazon-bedrock' ), 'warn', __( 'The selected model was not returned by the region catalog. Confirm the region offers it and whether an inference profile ID is required.', 'ai-chat-for-amazon-bedrock' ) ); } return $this->result( 'model', __( 'Model selection', 'ai-chat-for-amazon-bedrock' ), 'pass', $model ); } private function check_streaming( $options ) { $requested = ! isset( $options['enable_streaming'] ) || 'off' !== $options['enable_streaming']; $supported = AI_Chat_Bedrock_AWS::streaming_supported(); if ( ! $requested ) { return $this->result( 'streaming', __( 'Streaming', 'ai-chat-for-amazon-bedrock' ), 'warn', __( 'Streaming is disabled, so answers appear only when generation finishes.', 'ai-chat-for-amazon-bedrock' ) ); } if ( ! $supported ) { return $this->result( 'streaming', __( 'Streaming', 'ai-chat-for-amazon-bedrock' ), 'warn', __( 'The PHP cURL extension is unavailable, so buffered responses are used instead of streaming.', 'ai-chat-for-amazon-bedrock' ) ); } return $this->result( 'streaming', __( 'Streaming', 'ai-chat-for-amazon-bedrock' ), 'pass', __( 'Streaming responses are enabled with automatic fallback.', 'ai-chat-for-amazon-bedrock' ) ); } private function check_encryption() { $ok = function_exists( 'sodium_crypto_secretbox' ); return $this->result( 'encryption', __( 'Credential encryption', 'ai-chat-for-amazon-bedrock' ), $ok ? 'pass' : 'warn', $ok ? __( 'Stored credentials are encrypted with authenticated encryption.', 'ai-chat-for-amazon-bedrock' ) : __( 'Sodium is unavailable, so credentials cannot be encrypted in the database. Use wp-config.php constants or an IAM role.', 'ai-chat-for-amazon-bedrock' ) ); } private function check_public_access( $options ) { if ( empty( $options['allow_public_chat'] ) ) { return $this->result( 'guest_access', __( 'Guest access', 'ai-chat-for-amazon-bedrock' ), 'pass', __( 'Only signed-in users can start paid Bedrock requests.', 'ai-chat-for-amazon-bedrock' ) ); } $limit = isset( $options['rate_limit_per_minute'] ) ? absint( $options['rate_limit_per_minute'] ) : 5; // Report the limit guests actually get, which a per-role override can change. $guest_limit = $limit; if ( class_exists( 'AI_Chat_Bedrock_Rate_Limits' ) ) { $overrides = AI_Chat_Bedrock_Rate_Limits::all(); if ( isset( $overrides[ AI_Chat_Bedrock_Rate_Limits::GUEST_KEY ] ) ) { $guest_limit = (int) $overrides[ AI_Chat_Bedrock_Rate_Limits::GUEST_KEY ]; } } return $this->result( 'guest_access', __( 'Guest access', 'ai-chat-for-amazon-bedrock' ), 'warn', sprintf( /* translators: %s: requests allowed per visitor per minute. */ _n( 'Guest chat is enabled with %s request per visitor each minute. Confirm AWS Budgets and model pricing.', 'Guest chat is enabled with %s requests per visitor each minute. Confirm AWS Budgets and model pricing.', max( 1, $guest_limit ), 'ai-chat-for-amazon-bedrock' ), number_format_i18n( max( 1, $guest_limit ) ) ) ); } /** * Whether a configured guardrail actually exists and is ready. * * Bedrock fails closed here: a wrong identifier makes every request fail with a * ValidationException, so the chat stops working entirely. Nothing checked it before, * which meant finding out from a visitor. * * @param array $options Plugin settings. * @return array */ private function check_guardrail( $options ) { $label = __( 'Guardrail', 'ai-chat-for-amazon-bedrock' ); $identifier = isset( $options['guardrail_id'] ) ? trim( (string) $options['guardrail_id'] ) : ''; if ( '' === $identifier ) { return $this->result( 'guardrail', $label, 'off', __( 'No guardrail is configured, so nothing is filtered by Amazon Bedrock.', 'ai-chat-for-amazon-bedrock' ) ); } $version = isset( $options['guardrail_version'] ) ? trim( (string) $options['guardrail_version'] ) : ''; $aws = new AI_Chat_Bedrock_AWS(); if ( ! $aws->has_credentials() ) { return $this->result( 'guardrail', $label, 'warn', __( 'A guardrail is configured but credentials are missing, so it cannot be verified.', 'ai-chat-for-amazon-bedrock' ) ); } $guardrail = $aws->get_guardrail( $identifier, $version ); if ( is_wp_error( $guardrail ) ) { return $this->result( 'guardrail', $label, 'fail', sprintf( /* translators: %s: reason the guardrail could not be read. */ __( 'The configured guardrail could not be read, so every chat request will be refused: %s', 'ai-chat-for-amazon-bedrock' ), $guardrail->get_error_message() ) ); } if ( '' !== $guardrail['status'] && 'READY' !== strtoupper( $guardrail['status'] ) ) { return $this->result( 'guardrail', $label, 'warn', sprintf( /* translators: 1: guardrail name, 2: guardrail status reported by AWS. */ __( 'Guardrail "%1$s" is %2$s rather than READY.', 'ai-chat-for-amazon-bedrock' ), $guardrail['name'], $guardrail['status'] ) ); } return $this->result( 'guardrail', $label, 'pass', sprintf( /* translators: 1: guardrail name, 2: guardrail version. */ __( 'Guardrail "%1$s" is ready, version %2$s.', 'ai-chat-for-amazon-bedrock' ), $guardrail['name'], '' !== $guardrail['version'] ? $guardrail['version'] : 'DRAFT' ) ); } /** * Whether a configured knowledge base ID can work. * * The identifier has a fixed shape, so an obvious typo is caught without calling AWS at * all. Anything that could be real is then tried for real, since a plausible but wrong * ID otherwise fails silently the first time a visitor asks something. * * @param array $options Plugin settings. * @return array */ private function check_knowledge_base( $options ) { $label = __( 'Knowledge base', 'ai-chat-for-amazon-bedrock' ); $id = isset( $options['knowledge_base_id'] ) ? trim( (string) $options['knowledge_base_id'] ) : ''; if ( '' === $id ) { return $this->result( 'knowledge_base', $label, 'off', __( 'No Amazon Bedrock knowledge base is configured.', 'ai-chat-for-amazon-bedrock' ) ); } // AWS requires ten alphanumeric characters, or a knowledge base ARN. if ( ! preg_match( '#^[0-9a-zA-Z]{10}$#', $id ) && ! preg_match( '#^arn:aws(-[^:]+)?:bedrock:[a-z0-9-]{1,20}:[0-9]{12}:knowledge-base/[0-9a-zA-Z]{10}$#', $id ) ) { return $this->result( 'knowledge_base', $label, 'fail', __( 'The knowledge base ID is not the right shape. AWS expects ten letters or digits, or a knowledge base ARN.', 'ai-chat-for-amazon-bedrock' ) ); } $aws = new AI_Chat_Bedrock_AWS(); if ( ! $aws->has_credentials() ) { return $this->result( 'knowledge_base', $label, 'warn', __( 'A knowledge base is configured but credentials are missing, so it cannot be verified.', 'ai-chat-for-amazon-bedrock' ) ); } $result = $aws->retrieve_from_knowledge_base( $id, 'test', 1 ); if ( is_wp_error( $result ) ) { return $this->result( 'knowledge_base', $label, 'fail', sprintf( /* translators: %s: reason the knowledge base could not be queried. */ __( 'The knowledge base could not be queried: %s', 'ai-chat-for-amazon-bedrock' ), $result->get_error_message() ) ); } return $this->result( 'knowledge_base', $label, 'pass', __( 'The knowledge base answered a test query.', 'ai-chat-for-amazon-bedrock' ) ); } /** * Whether the semantic search index can be used. * * @param array $options Plugin settings. * @return array */ private function check_vector_store( $options ) { $label = __( 'Semantic search index', 'ai-chat-for-amazon-bedrock' ); $model = isset( $options['embedding_model_id'] ) ? (string) $options['embedding_model_id'] : ''; if ( '' === $model || ! class_exists( 'AI_Chat_Bedrock_Embeddings' ) ) { return $this->result( 'vector_store', $label, 'off', __( 'Semantic search is off; answers use keyword search.', 'ai-chat-for-amazon-bedrock' ) ); } if ( 's3_vectors' !== AI_Chat_Bedrock_Embeddings::store( $options ) ) { $status = AI_Chat_Bedrock_Embeddings::status( $model ); if ( (int) $status['total'] > AI_Chat_Bedrock_Embeddings::MAX_CANDIDATES ) { return $this->result( 'vector_store', $label, 'warn', sprintf( /* translators: 1: published item count, 2: items compared per question. */ __( 'Vectors are stored in the WordPress database, which compares a question with %2$s of the %1$s published items. Amazon S3 Vectors searches all of them.', 'ai-chat-for-amazon-bedrock' ), number_format_i18n( (int) $status['total'] ), number_format_i18n( AI_Chat_Bedrock_Embeddings::MAX_CANDIDATES ) ) ); } return $this->result( 'vector_store', $label, 'pass', __( 'Vectors are stored in the WordPress database.', 'ai-chat-for-amazon-bedrock' ) ); } if ( ! AI_Chat_Bedrock_S3_Vectors::enabled( $options ) ) { return $this->result( 'vector_store', $label, 'fail', __( 'Amazon S3 Vectors is chosen, but the bucket or index name is missing, so semantic search is off.', 'ai-chat-for-amazon-bedrock' ) ); } $index = AI_Chat_Bedrock_S3_Vectors::describe_index( $model, $options ); if ( is_wp_error( $index ) ) { return $this->result( 'vector_store', $label, 'fail', sprintf( /* translators: %s: reason the index could not be read. */ __( 'The S3 Vectors index could not be read: %s', 'ai-chat-for-amazon-bedrock' ), $index->get_error_message() ) ); } if ( ! empty( $index['problems'] ) ) { return $this->result( 'vector_store', $label, 'fail', AI_Chat_Bedrock_Translation::sentences( ...$index['problems'] ) ); } $message = __( 'The S3 Vectors index exists and matches the embedding model.', 'ai-chat-for-amazon-bedrock' ); if ( 'classic' === $index['mode'] ) { $message = AI_Chat_Bedrock_Translation::sentences( $message, __( 'It is a CLASSIC index, which applies metadata filters during the similarity search. The plugin asks for filtering first on every query; to make that the index default, switch it to ENHANCED with UpdateIndexMode.', 'ai-chat-for-amazon-bedrock' ) ); } return $this->result( 'vector_store', $label, 'pass', $message ); } /** * Whether WordPress runs the plugin's background tasks on time. * * Scheduled WeChat drafts, YouTube uploads, indexing and clean-ups run through WP-Cron, * which runs only when someone visits and which a page cache or DISABLE_WP_CRON without a * server cron job can stop altogether. Nothing failed then, so nothing said so. * * @return array */ private function check_schedules() { $label = __( 'Background tasks', 'ai-chat-for-amazon-bedrock' ); $late = 0; $oldest = 0; $cutoff = time() - 15 * MINUTE_IN_SECONDS; foreach ( (array) ( function_exists( 'wp_get_ready_cron_jobs' ) ? wp_get_ready_cron_jobs() : array() ) as $time => $hooks ) { foreach ( (array) $hooks as $hook => $events ) { if ( 0 === strpos( (string) $hook, 'ai_chat_bedrock_' ) && (int) $time < $cutoff ) { $late += count( (array) $events ); $oldest = $oldest ? min( $oldest, (int) $time ) : (int) $time; } } } if ( $late > 0 ) { $advice = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ? __( 'WP-Cron is turned off with DISABLE_WP_CRON, so a server cron job has to call wp-cron.php, or run wp cron event run --due-now, every few minutes.', 'ai-chat-for-amazon-bedrock' ) : __( 'WP-Cron runs only when someone visits, and a page cache can keep it from running. Have a server cron job call wp-cron.php every few minutes, then set DISABLE_WP_CRON.', 'ai-chat-for-amazon-bedrock' ); return $this->result( 'schedules', $label, 'warn', AI_Chat_Bedrock_Translation::sentences( sprintf( /* translators: 1: number of tasks, 2: how long ago the oldest was due, such as 3 hours. */ _n( '%1$s background task is overdue, by %2$s.', '%1$s background tasks are overdue, the oldest by %2$s.', $late, 'ai-chat-for-amazon-bedrock' ), number_format_i18n( $late ), human_time_diff( $oldest, time() ) ), $advice ) ); } // The recurring tasks, by name, so the check needs none of their classes loaded. foreach ( array( 'ai_chat_bedrock_wechat_drafts', 'ai_chat_bedrock_index_embeddings', 'ai_chat_bedrock_prune_leads', 'ai_chat_bedrock_prune_chat_history' ) as $hook ) { if ( wp_next_scheduled( $hook ) ) { return $this->result( 'schedules', $label, 'pass', __( 'The plugin\'s background tasks run on time.', 'ai-chat-for-amazon-bedrock' ) ); } } return $this->result( 'schedules', $label, 'off', __( 'No background tasks are scheduled.', 'ai-chat-for-amazon-bedrock' ) ); } private function check_mcp() { if ( ! get_option( 'ai_chat_bedrock_enable_mcp', false ) ) { return $this->result( 'mcp', __( 'MCP tools', 'ai-chat-for-amazon-bedrock' ), 'off', __( 'MCP tools are disabled.', 'ai-chat-for-amazon-bedrock' ) ); } if ( get_option( 'ai_chat_bedrock_mcp_public_access', false ) ) { return $this->result( 'mcp', __( 'MCP tools', 'ai-chat-for-amazon-bedrock' ), 'warn', __( 'The built-in WordPress MCP endpoint is publicly readable. Disable public access unless it is required.', 'ai-chat-for-amazon-bedrock' ) ); } return $this->result( 'mcp', __( 'MCP tools', 'ai-chat-for-amazon-bedrock' ), 'pass', __( 'MCP tools are enabled and require authentication.', 'ai-chat-for-amazon-bedrock' ) ); } /** * What is turned on for AI agents and tools, in one line, with where to turn the rest on. * It reports the settings, not whether each address answers right now. The MCP check above * already warns about public access, so this one only reports. */ private function check_agents() { $surface = AI_Chat_Bedrock_Ontology::agent_surface(); $label = __( 'AI agents', 'ai-chat-for-amazon-bedrock' ); $on = array(); $public = false; if ( $surface['site_description']['on'] ) { $on[] = __( 'the site description', 'ai-chat-for-amazon-bedrock' ); $public = true; } if ( $surface['site_abilities']['on'] ) { $on[] = __( 'the site content abilities', 'ai-chat-for-amazon-bedrock' ); $public = true; } $file = ''; if ( $surface['llms_txt']['on'] && ! $surface['llms_txt']['file'] ) { /* translators: %s: the llms.txt address. */ $on[] = sprintf( __( 'llms.txt at %s', 'ai-chat-for-amazon-bedrock' ), $surface['llms_txt']['url'] ); $public = true; } elseif ( $surface['llms_txt']['file'] ) { $file = __( 'An llms.txt file at the site root, not written by this plugin, is served to AI tools.', 'ai-chat-for-amazon-bedrock' ); } // The MCP server answers with its own sign-in and tool permissions, so it is listed with // how it is reached, and the public-content rule is not claimed for it. if ( $surface['mcp']['on'] ) { $on[] = $surface['mcp']['public'] ? __( 'the WordPress MCP server, readable without signing in', 'ai-chat-for-amazon-bedrock' ) : __( 'the WordPress MCP server, for signed-in accounts', 'ai-chat-for-amazon-bedrock' ); } if ( empty( $on ) ) { return $this->result( 'agents', $label, 'off', AI_Chat_Bedrock_Translation::sentences( __( 'AI agents and tools get nothing from the plugin. The site description, llms.txt, site content abilities and the MCP server are under Settings > Agents and tools.', 'ai-chat-for-amazon-bedrock' ), $file ) ); } return $this->result( 'agents', $label, 'pass', AI_Chat_Bedrock_Translation::sentences( /* translators: %s: what is turned on, such as "the site description, llms.txt at https://example.com/llms.txt". */ sprintf( __( 'Turned on for AI agents and tools: %s.', 'ai-chat-for-amazon-bedrock' ), AI_Chat_Bedrock_Translation::items( $on ) ), $public ? __( 'The site description, the site content abilities and the plugin\'s llms.txt show only published, public content, never drafts or members-only text.', 'ai-chat-for-amazon-bedrock' ) : '', $file ) ); } private function check_live_invocation( $options ) { $aws = new AI_Chat_Bedrock_AWS(); $result = $aws->test_model_access( isset( $options['model_id'] ) ? $options['model_id'] : '' ); if ( empty( $result['success'] ) ) { return $this->result( 'invocation', __( 'Model invocation', 'ai-chat-for-amazon-bedrock' ), 'fail', $result['message'] ); } $message = $result['message']; if ( isset( $result['duration'] ) ) { /* translators: %d: round trip duration in milliseconds. */ $message = AI_Chat_Bedrock_Translation::sentences( $message, sprintf( __( 'Round trip took %d ms.', 'ai-chat-for-amazon-bedrock' ), (int) $result['duration'] ) ); } if ( isset( $result['usage']['input_tokens'], $result['usage']['output_tokens'] ) ) { $message = AI_Chat_Bedrock_Translation::sentences( $message, sprintf( /* translators: 1: input token count, 2: output token count. */ __( 'Tokens used: %1$d input, %2$d output.', 'ai-chat-for-amazon-bedrock' ), (int) $result['usage']['input_tokens'], (int) $result['usage']['output_tokens'] ) ); } return $this->result( 'invocation', __( 'Model invocation', 'ai-chat-for-amazon-bedrock' ), 'pass', $message ); } private function result( $id, $label, $status, $message ) { return array( 'id' => sanitize_key( $id ), 'label' => $label, 'status' => in_array( $status, array( 'pass', 'off', 'warn', 'fail' ), true ) ? $status : 'warn', 'message' => $message, ); } }