*/ private array $editor_tools = array(); /** @var array Preloaded editor resources (Elementor). */ private array $editor_resources = array(); /** Whether legacy HTML conversion tools are available in this Elementor pass. */ private bool $allow_elementor_html_fallback = true; /** @var array{key:string,lock_key:string,envelope_hash:string,results_hash:string}|null */ private ?array $continuation_claim = null; private function is_available(): bool { return Compat::supports_ai(); } /** * Ensure the current user may edit the given post when a post ID is present. * * @param int|null $post_id Post ID or null. * @return true|WP_Error */ private function assert_can_edit_optional_post( ?int $post_id ) { if ( $post_id && ! Security::can_edit_post( $post_id ) ) { return new WP_Error( 'edit_forbidden', __( 'Sorry, you are not allowed to edit this post.', 'advanced-animation' ) ); } return true; } /** * Track Unsplash downloads when generated HTML embeds Unsplash CDN images. * * @param string|WP_Error $html Generated HTML or error. * @return string|WP_Error */ private function track_unsplash_in_html( $html ) { if ( ! is_wp_error( $html ) && is_string( $html ) && '' !== $html ) { \AdvancedAddons\Tools\Search_Unsplash_Image_Tool::track_downloads_in_content( $html ); } return $html; } /** * Run the agent loop. * * @param string $prompt Current user prompt. * @param string|null $provider AI provider ID. * @param string|null $model AI model ID. * @param array $attachments Files/images attached to the current message. * @param array $messages Previous conversation history. * @param array|null $selection Current editor selection context. * @param int|null $post_id Current editor post ID. * @param string|null $page_content Live page content from the editor. * @return array|WP_Error Result envelope with optional tool_calls log. */ public function run( string $prompt, ?string $provider = null, ?string $model = null, array $attachments = [], array $messages = [], ?array $selection = null, ?int $post_id = null, ?string $page_content = null, bool $use_global_theme = false, string $editor = "gutenberg", string $block_type_preference = 'auto', array $editor_tools = array(), ?array $continuation = null, array $editor_tool_results = array(), array $editor_resources = array() ) { if ( ! $this->is_available() ) { return new WP_Error( 'ai_unavailable', __( 'WordPress AI Client is not available on this site.', 'advanced-animation' ) ); } if ( $post_id && ! Security::can_edit_post( $post_id ) ) { return new WP_Error( 'edit_forbidden', __( 'Sorry, you are not allowed to edit this post.', 'advanced-animation' ) ); } $this->provider = $provider; $this->model = $model; $this->attachments = $attachments; $this->page_content = $page_content; $this->use_global_theme = $use_global_theme; $this->editor = $editor; $this->block_type_preference = $this->normalize_block_type_preference( $block_type_preference ); $this->editor_tools = 'elementor' === $editor ? $this->sanitize_editor_tools( $editor_tools ) : array(); $this->editor_resources = 'elementor' === $editor ? $this->sanitize_editor_resources( $editor_resources ) : array(); $direct_result = empty( $this->editor_tools ) ? $this->maybe_run_direct_editor_action( $prompt, $selection, $post_id, $attachments ) : null; if ( is_wp_error( $direct_result ) ) { return $direct_result; } if ( is_array( $direct_result ) ) { return $direct_result; } $continuation_state = $this->restore_editor_continuation( $continuation, $editor_tool_results, $post_id ); if ( is_wp_error( $continuation_state ) ) { return $continuation_state; } $native_error_count = $this->next_native_error_count( $continuation_state, $editor_tool_results ); $this->allow_elementor_html_fallback = 'elementor' !== $editor || empty( $this->editor_tools ) || ( null !== $continuation && $native_error_count >= self::ELEMENTOR_NATIVE_ERROR_FALLBACK_THRESHOLD ); $registry = $this->build_tool_registry(); $declarations = $this->build_function_declarations( $registry ); if ( $continuation_state ) { $conversation = $continuation_state['conversation']; $tool_log = $continuation_state['tool_log']; $start_step = $continuation_state['step']; $use_native = $continuation_state['use_native']; } else { $conversation = array_merge( $this->normalize_history( $messages ), array( $this->build_user_message( $prompt, $attachments ) ) ); $initial_text = $this->message_to_text( end( $conversation ) ?: array() ); $use_native = $this->detect_native_support( $initial_text, $declarations ); $tool_log = array(); $start_step = 0; } $system_prompt = $this->build_system_prompt( $registry, $selection, $use_native, $post_id ); $max_steps = self::ELEMENTOR_MAX_TOOL_STEPS; $narration_nudges = 0; for ( $step = $start_step; $step < $max_steps; $step++ ) { $ai_turn = $this->call_ai_with_rate_limit_retry( $system_prompt, $conversation, 0 === $step && ! $continuation_state, $declarations, $use_native ); if ( is_wp_error( $ai_turn ) ) { $this->release_editor_continuation_claim(); return $ai_turn; } $this->consume_editor_continuation_claim(); // Native function-call turn. if ( 'function_calls' === $ai_turn['type'] ) { $model_parts = array(); $response_parts = array(); $deferred_calls = array(); foreach ( $ai_turn['calls'] as $call ) { $tool_name = $call['name'] ?? ''; $call_id = sanitize_text_field( strval( $call['id'] ?? '' ) ); if ( '' === $call_id ) { $call_id = wp_generate_uuid4(); } $call_args = is_array( $call['args'] ?? null ) ? $call['args'] : array(); $tool = $registry->get( $tool_name ); if ( ! $tool ) { $tool_result = array( 'success' => false, /* translators: %s: tool name requested by the AI model */ 'error' => sprintf( __( 'Unknown tool requested: %s', 'advanced-animation' ), $tool_name ), ); } elseif ( $tool instanceof Deferred_Editor_Tool ) { $tool_result = null; $deferred_calls[] = array( 'id' => $call_id, 'name' => $tool_name, 'args' => $call_args, ); } else { $arguments = $call_args; $tool_context = array( 'post_id' => $post_id, 'selection' => $selection, 'page_content' => $this->page_content, 'editor' => $this->editor, 'block_type_preference' => $this->block_type_preference, 'request_source' => 'chat', ); $tool_result = $tool->execute( $arguments, $tool_context ); } if ( ! ( $tool instanceof Deferred_Editor_Tool ) ) { $tool_log[] = array( 'tool' => $tool_name, 'arguments' => $call_args, 'result' => $tool_result, ); } $model_part = array( 'type' => 'functionCall', 'functionCall' => array( 'id' => $call_id, 'name' => $tool_name, 'args' => $call_args, ), ); if ( ! empty( $call['thoughtSignature'] ) ) { $model_part['thoughtSignature'] = $call['thoughtSignature']; } $model_parts[] = $model_part; if ( ! ( $tool instanceof Deferred_Editor_Tool ) ) { $response_parts[] = array( 'type' => 'functionResponse', 'functionResponse' => array( 'id' => $call_id, 'name' => $tool_name, 'response' => $tool_result, ), ); } } $conversation[] = array( 'role' => 'model', 'parts' => $model_parts, ); if ( ! empty( $deferred_calls ) ) { return $this->build_editor_tool_calls_response( $deferred_calls, $conversation, $response_parts, $tool_log, $step + 1, $use_native, $post_id, 'native', $native_error_count ); } foreach ( $response_parts as $response_part ) { $conversation[] = array( 'role' => 'user', 'parts' => array( $response_part ), ); } continue; } // Text response (native final answer or manual fallback). $ai_response = $ai_turn['text'] ?? ''; $parsed = $this->parse_response( $ai_response ); if ( is_wp_error( $parsed ) ) { return $parsed; } $action = $parsed['action'] ?? ''; if ( 'final' === $action ) { $result = $parsed['result'] ?? array(); if ( ! is_array( $result ) ) { $result = array( 'type' => 'message', 'message' => strval( $result ) ); } // Elementor builds must end in a tool call or a result envelope. // When the model only narrates the steps it is about to take // (e.g. "Fetching the required Elementor resources first, then // I'll build one complete hero section composition."), feed the // narration back and force it to actually act. The max-steps // guard keeps the loop bounded. if ( 'elementor' === $this->editor && 'message' === ( $result['type'] ?? '' ) && $narration_nudges < self::ELEMENTOR_MAX_NARRATION_NUDGES && $this->is_narration_message( strval( $result['message'] ?? '' ) ) ) { $narration_nudges++; $conversation[] = array( 'role' => 'model', 'parts' => array( array( 'type' => 'text', 'text' => $ai_response ), ), ); $conversation[] = array( 'role' => 'user', 'parts' => array( array( 'type' => 'text', 'text' => 'You only described the next step but did not perform it. Continue now and complete the user\'s request: call the required tool(s) or return the final result envelope (e.g. {"action":"tool_call","tool":"...","arguments":{...}} or {"type":"blocks","html":"..."}). Do not narrate what you will do — perform the action now.', ), ), ); continue; } $result['tool_calls'] = $tool_log; return $result; } if ( 'tool_call' === $action ) { $tool_name = $parsed['tool'] ?? ''; $arguments = $parsed['arguments'] ?? array(); $tool = $registry->get( $tool_name ); if ( ! $tool ) { return new WP_Error( 'unknown_tool', /* translators: %s: tool name requested by the AI model */ sprintf( __( 'Unknown tool requested: %s', 'advanced-animation' ), $tool_name ) ); } if ( $tool instanceof Deferred_Editor_Tool ) { $conversation[] = array( 'role' => 'model', 'parts' => array( array( 'type' => 'text', 'text' => wp_json_encode( $parsed ) ), ), ); return $this->build_editor_tool_calls_response( array( array( 'id' => 'manual-' . wp_generate_uuid4(), 'name' => $tool_name, 'args' => $arguments, ), ), $conversation, array(), $tool_log, $step + 1, false, $post_id, 'manual', $native_error_count ); } $tool_context = array( 'post_id' => $post_id, 'selection' => $selection, 'page_content' => $this->page_content, 'editor' => $this->editor, 'block_type_preference' => $this->block_type_preference, 'request_source' => 'chat', ); $tool_result = $tool->execute( $arguments, $tool_context ); $tool_log[] = array( 'tool' => $tool_name, 'arguments' => $arguments, 'result' => $tool_result, ); $conversation[] = array( 'role' => 'model', 'parts' => array( array( 'type' => 'text', 'text' => json_encode( $parsed ) ), ), ); $conversation[] = array( 'role' => 'user', 'parts' => array( array( 'type' => 'text', 'text' => json_encode( array( 'tool_result' => $tool_result, ) ), ), ), ); continue; } return new WP_Error( 'invalid_action', __( 'AI response did not contain a valid action.', 'advanced-animation' ) ); } return new WP_Error( 'max_steps', __( 'The AI reached the maximum number of tool steps.', 'advanced-animation' ) ); } /** * Deterministically handle obvious editor mutations before entering the * agent loop. This lets smaller models generate content without requiring * them to reliably orchestrate tool calls. * * @return array|WP_Error|null Direct tool result, error, or null to use the agent loop. */ private function maybe_run_direct_editor_action( string $prompt, ?array $selection, ?int $post_id, array $attachments ) { if ( ! empty( $attachments ) ) { return null; } $intent = $this->classify_editor_intent( $prompt, $selection ); if ( null === $intent ) { return null; } if ( empty( $post_id ) && in_array( $intent['action'], array( 'add', 'full_page' ), true ) ) { return new WP_Error( 'no_current_post', __( 'No current post ID available for this editor action.', 'advanced-animation' ) ); } if ( 'replace' === $intent['action'] && empty( $selection ) ) { return array( 'type' => 'error', 'message' => __( 'Select a block or element in the editor first, then ask me to replace or update it.', 'advanced-animation' ), ); } $tool = $this->tool_for_direct_intent( $intent['action'] ); if ( ! $tool ) { return null; } $arguments = array( 'description' => $this->build_direct_description( $prompt, $intent ), ); if ( 'add' === $intent['action'] ) { $arguments['position'] = $intent['position']; if ( 'gutenberg' === $this->editor ) { $arguments['block_type'] = $this->resolve_block_type_for_prompt( $prompt ); } } elseif ( 'replace' === $intent['action'] && 'gutenberg' === $this->editor ) { $arguments['block_type'] = $this->resolve_block_type_for_prompt( $prompt ); } $context = array( 'post_id' => $post_id, 'selection' => $selection, 'page_content' => $this->page_content, 'editor' => $this->editor, 'block_type_preference' => $this->block_type_preference, 'request_source' => 'chat', ); $result = $tool->execute( $arguments, $context ); $result['tool_calls'] = array( array( 'tool' => $tool->name(), 'arguments' => $arguments, 'result' => $result, 'orchestration' => 'direct', ), ); if ( empty( $result['message'] ) ) { $result['message'] = $this->direct_success_message( $intent['action'] ); } return $result; } /** * Classify common editor commands that should not depend on model tool calling. */ private function classify_editor_intent( string $prompt, ?array $selection ): ?array { $text = strtolower( trim( wp_strip_all_tags( $prompt ) ) ); if ( '' === $text ) { return null; } if ( $this->contains_any( $text, array( 'do not insert', "don't insert", 'without inserting', 'preview only', 'just show', 'show me html', 'raw html' ) ) ) { return null; } $mutation_words = array( 'add', 'insert', 'create', 'generate', 'build', 'make', 'put', 'replace', 'update', 'change', 'edit', 'modify', 'restyle', 'redesign', 'rewrite', 'turn', 'convert', 'transform', ); if ( ! $this->contains_any( $text, $mutation_words ) ) { return null; } if ( $this->contains_any( $text, array( 'whole page', 'entire page', 'full page', 'all sections', 'every section', 'all blocks', 'all headings', 'every heading', 'all text', 'every text', 'everything', 'sitewide', 'reorder', 'rearrange', 'swap sections', 'rewrite the page', 'redesign the page' ) ) ) { return array( 'action' => 'full_page', 'position' => 'end', ); } $replace_words = array( 'replace', 'update', 'change', 'edit', 'modify', 'restyle', 'redesign', 'rewrite', 'turn', 'convert', 'transform', 'make' ); $selected_refs = array( 'selected', 'selection', 'this', 'that', 'this block', 'this section', 'this element', 'current block', 'current section', 'current element', 'it', 'dark mode', 'light mode' ); if ( ! empty( $selection ) && $this->contains_any( $text, $replace_words ) && $this->contains_any( $text, $selected_refs ) ) { return array( 'action' => 'replace', 'position' => 'end', ); } if ( ! empty( $selection ) && $this->contains_any( $text, array( 'dark mode', 'light mode', 'make it dark', 'make it light' ) ) ) { return array( 'action' => 'replace', 'position' => 'end', ); } $add_words = array( 'add', 'insert', 'create', 'generate', 'build', 'make', 'put' ); $add_targets = array( 'section', 'hero', 'banner', 'cta', 'call to action', 'feature', 'features', 'pricing', 'testimonial', 'card', 'cards', 'gallery', 'faq', 'footer', 'header', 'component', 'block' ); if ( $this->contains_any( $text, $add_words ) && $this->contains_any( $text, $add_targets ) ) { return array( 'action' => 'add', 'position' => $this->detect_insert_position( $text ), ); } return null; } private function tool_for_direct_intent( string $action ): ?Tool { if ( 'elementor' === $this->editor ) { if ( 'add' === $action ) { return new Elementor_Add_Html_To_Page_Tool( $this ); } if ( 'replace' === $action ) { return new Elementor_Replace_Selection_Tool( $this ); } if ( 'full_page' === $action ) { return new Elementor_Edit_Full_Page_Tool( $this ); } return null; } if ( 'add' === $action ) { return new Add_Blocks_To_Page_Tool( $this ); } if ( 'replace' === $action ) { return new Replace_Selected_Blocks_Tool( $this ); } if ( 'full_page' === $action ) { return new Edit_Full_Page_Tool( $this ); } return null; } private function detect_insert_position( string $text ): string { if ( $this->contains_any( $text, array( 'at the top', 'to the top', 'above everything', 'before everything', 'beginning of the page', 'start of the page' ) ) ) { return 'start'; } if ( $this->contains_any( $text, array( 'before selected', 'before this', 'above selected', 'above this' ) ) ) { return 'before_selection'; } if ( $this->contains_any( $text, array( 'after selected', 'after this', 'below selected', 'below this', 'under selected', 'under this' ) ) ) { return 'after_selection'; } return 'end'; } private function detect_native_block_request( string $prompt ): bool { $text = strtolower( $prompt ); return $this->contains_any( $text, array( 'blog posts', 'latest posts', 'post list', 'wordpress native block', 'wordpress native blocks', 'native block', 'native blocks', 'gutenberg block', 'gutenberg blocks', 'core block', 'core blocks', 'wordpress block', 'wordpress blocks', 'query loop', 'archive', 'search form', 'navigation', 'menu', 'post content', 'dynamic', ) ); } private function normalize_block_type_preference( string $preference ): string { $preference = strtolower( trim( $preference ) ); return in_array( $preference, array( 'auto', 'tailwind', 'native' ), true ) ? $preference : 'auto'; } private function resolve_block_type_for_prompt( string $prompt ): string { if ( 'native' === $this->block_type_preference ) { return 'native'; } if ( 'tailwind' === $this->block_type_preference ) { return 'tailwind'; } return $this->detect_native_block_request( $prompt ) ? 'native' : 'tailwind'; } private function build_direct_description( string $prompt, array $intent ): string { $description = trim( $prompt ); if ( 'replace' === $intent['action'] ) { return $description . "\n\nGenerate a complete replacement for the current selection. Preserve the original purpose and content unless the user explicitly asks to change it."; } if ( 'full_page' === $intent['action'] ) { return $description . "\n\nApply this request to the complete current page."; } return $description; } private function direct_success_message( string $action ): string { if ( 'replace' === $action ) { return __( 'Selected content updated.', 'advanced-animation' ); } if ( 'full_page' === $action ) { return __( 'Page updated.', 'advanced-animation' ); } return __( 'Content added to the page.', 'advanced-animation' ); } private function contains_any( string $text, array $needles ): bool { foreach ( $needles as $needle ) { if ( '' !== $needle && false !== strpos( $text, $needle ) ) { return true; } } return false; } /** * Detect a text-only assistant response that describes an upcoming action * instead of performing it, e.g. "Fetching the required Elementor resources * first, then I'll build one complete hero section composition." * * In the Elementor builder a narration is never a usable final answer, so * the agent loop nudges the model to continue. Completed summaries and * clarification questions are left alone. * * @param string $message Text-only assistant response. * @return bool True when the message is an unfinished plan of action. */ private function is_narration_message( string $message ): bool { $text = strtolower( trim( $message ) ); if ( '' === $text ) { return true; } if ( '?' === substr( $text, -1 ) ) { return false; } // Real answers are long (summaries, explanations, lists); narrations // are short promises of the next step. Never nudge long responses. if ( strlen( $text ) > 350 ) { return false; } // Completed work reports must never be treated as narration. $completed = array( 'is built and live', 'fully built', 'has been built', 'is now built', 'built and verified', 'is live on', 'are live on', 'is now live', "here's what i", 'here is what i', 'what would you like to build', 'what would you like next', 'has been added', 'added to the page', 'inserted into the page', 'updated successfully', ); if ( $this->contains_any( $text, $completed ) ) { return false; } $plans = array( 'i will', "i'll", "i'm going", 'i am going', 'let me', 'i need to', 'i have to', 'i want to', 'going to', 'about to', 'one moment', 'in a moment', 'just a moment', 'give me a moment', 'please wait', 'hold on', 'one second', ); $actions = array( 'fetching', 'fetch', 'gathering', 'gather', 'loading', 'load', 'preparing', 'prepare', 'retrieving', 'retrieve', 'getting', 'get the required', 'checking', 'check', 'inspecting', 'inspect', 'pulling', 'pull', 'reading', 'read the', 'looking at', 'examining', 'examine', ); $sequences = array( 'first', 'before', 'then', 'next', 'start by', 'begin by', 'after that', 'so i can', 'in order to', 'and then', 'while i', 'once i', 'after i', ); $has_plan = $this->contains_any( $text, $plans ); $has_action = $this->contains_any( $text, $actions ); $has_sequenc = $this->contains_any( $text, $sequences ); return ( $has_plan && $has_action ) || ( $has_plan && $has_sequenc ) || ( $has_action && $has_sequenc ); } /** * Generate raw Tailwind HTML for a given description. * * @param string $description Prompt describing the HTML to generate. * @param array|null $selection Optional selected block context. * @param int|null $post_id Current post ID for full-page context. * @return string|WP_Error HTML string or error. */ public function generate_html( string $description, ?array $selection = null, ?int $post_id = null ) { if ( ! $this->is_available() ) { return new WP_Error( 'ai_unavailable', __( 'WordPress AI Client is not available on this site.', 'advanced-animation' ) ); } $auth = $this->assert_can_edit_optional_post( $post_id ); if ( is_wp_error( $auth ) ) { return $auth; } $contextual = $this->build_contextual_prompt( $description, $selection, $post_id ); $system = 'You are a web developer building WordPress content. ' . 'Respond with a complete HTML document or section. ' . 'Use Tailwind CSS v4 utility classes for ALL styling (layout, typography, colors, spacing, responsive design). ' . 'Do NOT use "; } return $context; } catch ( \Exception $e ) { return ''; } } /** * Build a compact native-tool index for the Elementor system prompt. * * Native tool schemas are already sent through function declarations, so * repeating the full JSON schemas in the system instruction only doubles * token consumption. * * @param Tool_Registry $registry Tool registry for the current pass. * @return string Compact one-line-per-tool index. */ private function build_compact_elementor_tool_index( Tool_Registry $registry ): string { $lines = array(); foreach ( $registry->all() as $tool ) { $description = trim( strval( $tool->description() ) ); $description = $this->truncate_text( $description, 240 ); $lines[] = sprintf( '- %s: %s', $tool->name(), $description ); } return implode( "\n", $lines ); } private function build_elementor_system_prompt( Tool_Registry $registry, ?array $selection, ?int $post_id = null, bool $use_native = false ): string { if ( $use_native ) { $schemas = $this->build_compact_elementor_tool_index( $registry ); } else { $schemas = wp_json_encode( $registry->get_schemas(), JSON_PRETTY_PRINT ); } $selection_note = ''; if ( $selection && ! empty( $selection['elementData'] ) ) { $rendered = $this->render_selection_context( $selection['elementData'] ); if ( ! empty( $rendered ) ) { $selection_note = "\n\nCurrently selected element in the Elementor editor (rendered HTML + CSS context):\n" . $rendered; } } elseif ( $selection && ! empty( $selection['html'] ) ) { $selection_note = "\n\nCurrently selected element in the Elementor editor (use as context):\n" . $selection['html']; } $page_context = $this->build_page_context( $post_id, $selection ); $image_note = "\n- When building a section that needs images (hero backgrounds, feature images, cards, galleries), " . "FIRST call search_unsplash_image with descriptive keywords, then pass the returned image URLs to the applicable editor tool. " . "Do NOT invent placeholder image URLs."; $theme_context = $this->build_theme_context(); $has_native_tools = ! empty( $this->editor_tools ); if ( $has_native_tools && ! $this->allow_elementor_html_fallback ) { $resource_workflow = empty( $this->editor_resources ) ? '- Before composing or styling, call editor-resource-getter for every resource named by the applicable tool, then inspect the available V4 widget inventory, relevant schemas, and guides.\n' : '- The editor resources listed in AVAILABLE PRELOADED EDITOR RESOURCES are already loaded into this prompt, including the available V4 widget inventory and catalog-referenced widget schemas. Use those URIs directly; call editor-resource-getter only for a required URI that is not listed.\n'; $design_system_workflow = empty( $this->editor_resources ) ? '- Read the design-system resources before composing: elementor://global-classes (reuse existing global classes) and elementor://global-variables (the only global variables allowed).\n' : '- The design-system resources (elementor://global-classes and elementor://global-variables) are included in PRELOADED EDITOR RESOURCES below — reuse those global classes and variables instead of inventing colors, fonts, or classes.\n'; $editor_workflow = '- Native Elementor MCP tools are available and are the only page-editing tools enabled for this pass.\n' . $resource_workflow . $design_system_workflow . '- Reference global design tokens (globals/colors?id=..., globals/typography?id=...) and existing global classes in the xmlStructure instead of inventing colors, fonts, or classes.\n' . '- Request independent resources and schema reads together in the same function-calling turn so they can be resolved as one batch.\n' . '- For a new section, prefer one complete build-compositions call containing the full hierarchy and settings instead of creating elements over many rounds.\n' . '- Use native class and configuration tools afterward only for changes that cannot be represented in build-compositions.\n' . '- Do not generate HTML or ask to use HTML conversion while native tools are available.\n' . '- Native changes remain unsaved in the live editor. Never attempt to save or publish.\n' . "BUILD-COMPOSITIONS XML RULES (strict):\n" . "- xmlStructure must be well-formed XML with exactly ONE root element and unique configuration-id values.\n" . "- Escape every raw '&' as '&' in text content and attributes; never write raw '<' or '>' inside text.\n" . "- Do not include HTML, markdown fences, comments, or a trailing newline structure — only the XML string.\n" . "- Widget tags and prop values must come exactly from the widget schemas: enum props (e.g. flexbox html_tag) accept ONLY the listed values; a value like 'nav' or 'main' is rejected.\n" . "- If a tool call returns an XML parse error, read the parser reason in the error and fix the exact string before retrying.\n"; $fallback_tool_help = ''; $fallback_flow = ''; } else { $editor_workflow = $has_native_tools ? '- A native Elementor MCP operation failed during this turn. HTML conversion is now enabled as a compatibility fallback.\n' : '- Elementor MCP is unavailable. Use the HTML generation and conversion tools described below.\n'; $fallback_tool_help = "- generate_elementor_html — generate a new HTML section for preview.\n" . "- add_elementor_html_to_page — generate HTML and insert a new section into the page.\n" . "- replace_elementor_selection — replace the selected element using converted HTML.\n" . "- edit_elementor_full_page — regenerate the entire page using converted HTML.\n"; $fallback_flow = "\nHTML FALLBACK RULES:\n" . "- For one new section use add_elementor_html_to_page. For a selected-element edit use replace_elementor_selection.\n" . "- Use edit_elementor_full_page only for a whole-page rewrite.\n" . "- Always return renderable semantic HTML with one top-level