-
Notifications
You must be signed in to change notification settings - Fork 155
Add: AI-assisted payload generation to the Ability Explorer - Test Ability Screen #695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||
| <?php | ||||||
|
|
||||||
| /** | ||||||
| * Admin Page Class | ||||||
| * | ||||||
|
|
@@ -8,7 +9,7 @@ | |||||
| * @since 0.2.0 | ||||||
| */ | ||||||
|
|
||||||
| declare( strict_types=1 ); | ||||||
| declare(strict_types=1); | ||||||
|
|
||||||
| namespace WordPress\AI\Experiments\Abilities_Explorer; | ||||||
|
|
||||||
|
|
@@ -25,6 +26,7 @@ | |||||
| */ | ||||||
| class Admin_Page { | ||||||
|
|
||||||
|
|
||||||
| /** | ||||||
| * Initialize admin functionality. | ||||||
| * | ||||||
|
|
@@ -33,6 +35,7 @@ class Admin_Page { | |||||
| public function init(): void { | ||||||
| add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); | ||||||
| add_action( 'wp_ajax_ai_ability_explorer_invoke', array( $this, 'ajax_invoke_ability' ) ); | ||||||
| add_action( 'wp_ajax_ai_ability_explorer_generate_payload', array( $this, 'ajax_generate_payload' ) ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
|
|
@@ -300,12 +303,12 @@ private function render_test_runner(): void { | |||||
| <div class="notice notice-info inline" style="margin: 10px 0;"> | ||||||
| <p> | ||||||
| <strong><?php esc_html_e( 'How to test:', 'ai' ); ?></strong><br> | ||||||
| <ol> | ||||||
| <li><?php esc_html_e( 'Edit the JSON input below with your test data', 'ai' ); ?></li> | ||||||
| <li><?php esc_html_e( 'Click "Validate Input" to check your JSON is correct', 'ai' ); ?></li> | ||||||
| <li><?php esc_html_e( 'Click "Invoke Ability" to execute the ability with your input', 'ai' ); ?></li> | ||||||
| <li><?php esc_html_e( 'View the results below', 'ai' ); ?></li> | ||||||
| </ol> | ||||||
| <ol> | ||||||
| <li><?php esc_html_e( 'Edit the JSON input below with your test data', 'ai' ); ?></li> | ||||||
| <li><?php esc_html_e( 'Click "Validate Input" to check your JSON is correct', 'ai' ); ?></li> | ||||||
| <li><?php esc_html_e( 'Click "Invoke Ability" to execute the ability with your input', 'ai' ); ?></li> | ||||||
| <li><?php esc_html_e( 'View the results below', 'ai' ); ?></li> | ||||||
| </ol> | ||||||
|
Comment on lines
+306
to
+311
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was spacing changed here? |
||||||
| </p> | ||||||
| </div> | ||||||
| <?php endif; ?> | ||||||
|
|
@@ -323,6 +326,11 @@ private function render_test_runner(): void { | |||||
| <button type="button" id="ability-test-clear" class="button"> | ||||||
| <?php esc_html_e( 'Clear Result', 'ai' ); ?> | ||||||
| </button> | ||||||
| <?php if ( ! empty( $ability['input_schema'] ) ) : ?> | ||||||
| <button type="button" id="ability-test-generate-ai" class="button"> | ||||||
| <?php esc_html_e( 'Generate with AI', 'ai' ); ?> | ||||||
| </button> | ||||||
| <?php endif; ?> | ||||||
| </div> | ||||||
|
|
||||||
| <div id="ability-test-validation" class="ability-test-validation" style="display: none;"></div> | ||||||
|
|
@@ -483,6 +491,116 @@ public function ajax_invoke_ability(): void { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * AJAX handler for AI-assisted payload generation. | ||||||
| * | ||||||
| * @since 1.0.1 | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All |
||||||
| */ | ||||||
| public function ajax_generate_payload(): void { | ||||||
| // Verify nonce. | ||||||
| check_ajax_referer( 'ai_ability_explorer_generate_payload', 'nonce' ); | ||||||
|
|
||||||
| // Check user capabilities. | ||||||
| if ( ! current_user_can( 'manage_options' ) ) { | ||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => __( 'Insufficient permissions.', 'ai' ), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Get parameters. | ||||||
| $ability_slug = isset( $_POST['ability'] ) ? sanitize_text_field( wp_unslash( $_POST['ability'] ) ) : ''; | ||||||
| $command = isset( $_POST['command'] ) ? sanitize_textarea_field( wp_unslash( $_POST['command'] ) ) : ''; | ||||||
|
|
||||||
| if ( empty( $ability_slug ) ) { | ||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => __( 'Ability slug is required.', 'ai' ), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| if ( empty( $command ) ) { | ||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => __( 'A command is required.', 'ai' ), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Get ability to retrieve its input schema. | ||||||
| $ability = Ability_Handler::get_ability( $ability_slug ); | ||||||
|
|
||||||
| if ( ! $ability ) { | ||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => __( 'Ability not found.', 'ai' ), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| if ( ! function_exists( 'wp_ai_client_prompt' ) ) { | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function should always exist as WP 7.0 is our minimum |
||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => __( 'AI provider is not available.', 'ai' ), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| $schema_json = wp_json_encode( $ability['input_schema'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); | ||||||
|
|
||||||
| $system_instruction = 'You are a JSON payload generator. Given an input schema and a user command, generate a valid JSON object that satisfies the schema and fulfills the command. Return ONLY valid JSON with no explanation, no markdown, and no code fences.'; | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would suggest we use structured outputs instead of trying to get the LLM to return JSON via these instructions |
||||||
|
|
||||||
| $user_prompt = sprintf( | ||||||
| "Input Schema:\n%s\n\nUser Command: %s", | ||||||
| $schema_json, | ||||||
| $command | ||||||
| ); | ||||||
|
|
||||||
| $prompt_builder = wp_ai_client_prompt( $user_prompt ) | ||||||
| ->using_system_instruction( $system_instruction ); | ||||||
|
|
||||||
| if ( ! $prompt_builder->is_supported_for_text_generation() ) { | ||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => __( 'No AI provider available. Please connect one in the plugin settings.', 'ai' ), | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| $result = $prompt_builder->generate_text(); | ||||||
|
|
||||||
| if ( is_wp_error( $result ) ) { | ||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => $result->get_error_message(), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Strip markdown code fences if the model includes them. | ||||||
| $raw = trim( (string) $result ); | ||||||
| $raw = preg_replace( '/^```(?:json)?\s*/i', '', $raw ) ?? $raw; | ||||||
| $raw = preg_replace( '/\s*```$/', '', $raw ) ?? $raw; | ||||||
| $raw = trim( $raw ); | ||||||
|
|
||||||
| $parsed = json_decode( $raw, true ); | ||||||
| if ( json_last_error() !== JSON_ERROR_NONE ) { | ||||||
| wp_send_json_error( | ||||||
| array( | ||||||
| 'message' => __( 'The AI returned a response that could not be parsed as JSON. Please try again or rephrase your command.', 'ai' ), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| wp_send_json_success( | ||||||
| array( | ||||||
| 'payload' => wp_json_encode( $parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Add contextual help tabs to the screen. | ||||||
| * | ||||||
|
|
@@ -500,7 +618,7 @@ public function add_help_tabs(): void { | |||||
| 'id' => 'abilities-overview', | ||||||
| 'title' => __( 'Overview', 'ai' ), | ||||||
| 'content' => | ||||||
| '<p>' . esc_html__( 'Abilities are a standardized way for WordPress core, plugins, and themes to expose discrete units of functionality. Each ability has a name, optional input/output schemas, and can be invoked programmatically.', 'ai' ) . '</p>' . | ||||||
| '<p>' . esc_html__( 'Abilities are a standardized way for WordPress core, plugins, and themes to expose discrete units of functionality. Each ability has a name, optional input/output schemas, and can be invoked programmatically.', 'ai' ) . '</p>' . | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another spacing removal |
||||||
| '<p>' . esc_html__( 'The Abilities Explorer lets you browse every registered ability, inspect its schemas, and test it with custom input right from the admin.', 'ai' ) . '</p>', | ||||||
| ) | ||||||
| ); | ||||||
|
|
@@ -512,11 +630,11 @@ public function add_help_tabs(): void { | |||||
| 'id' => 'abilities-providers', | ||||||
| 'title' => esc_html__( 'Providers', 'ai' ), | ||||||
| 'content' => | ||||||
| '<p>' . esc_html__( 'Every ability is associated with a provider that indicates where it comes from:', 'ai' ) . '</p>' . | ||||||
| '<p>' . esc_html__( 'Every ability is associated with a provider that indicates where it comes from:', 'ai' ) . '</p>' . | ||||||
| '<ul>' . | ||||||
| '<li>' . wp_kses( __( '<strong>Core</strong>: Built into WordPress itself.', 'ai' ), $provider_tags ) . '</li>' . | ||||||
| '<li>' . wp_kses( __( '<strong>Plugin</strong>: Registered by an active plugin.', 'ai' ), $provider_tags ) . '</li>' . | ||||||
| '<li>' . wp_kses( __( '<strong>Theme</strong>: Registered by the active theme.', 'ai' ), $provider_tags ) . '</li>' . | ||||||
| '<li>' . wp_kses( __( '<strong>Core</strong>: Built into WordPress itself.', 'ai' ), $provider_tags ) . '</li>' . | ||||||
| '<li>' . wp_kses( __( '<strong>Plugin</strong>: Registered by an active plugin.', 'ai' ), $provider_tags ) . '</li>' . | ||||||
| '<li>' . wp_kses( __( '<strong>Theme</strong>: Registered by the active theme.', 'ai' ), $provider_tags ) . '</li>' . | ||||||
|
Comment on lines
+633
to
+637
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another spacing removal |
||||||
| '</ul>', | ||||||
| ) | ||||||
| ); | ||||||
|
|
@@ -526,19 +644,19 @@ public function add_help_tabs(): void { | |||||
| 'id' => 'abilities-testing', | ||||||
| 'title' => esc_html__( 'Testing', 'ai' ), | ||||||
| 'content' => | ||||||
| '<p>' . esc_html__( 'You can test any ability directly from this screen:', 'ai' ) . '</p>' . | ||||||
| '<p>' . esc_html__( 'You can test any ability directly from this screen:', 'ai' ) . '</p>' . | ||||||
| '<ol>' . | ||||||
| '<li>' . __( 'Click "Test" next to an ability in the list.', 'ai' ) . '</li>' . | ||||||
| '<li>' . __( 'Edit the pre-filled Input Data if the ability accepts JSON parameters.', 'ai' ) . '</li>' . | ||||||
| '<li>' . __( 'Use "Validate Input" to check your JSON against the schema.', 'ai' ) . '</li>' . | ||||||
| '<li>' . __( 'Click "Invoke Ability" to execute it and see the result.', 'ai' ) . '</li>' . | ||||||
| '<li>' . __( 'Click "Test" next to an ability in the list.', 'ai' ) . '</li>' . | ||||||
| '<li>' . __( 'Edit the pre-filled Input Data if the ability accepts JSON parameters.', 'ai' ) . '</li>' . | ||||||
| '<li>' . __( 'Use "Validate Input" to check your JSON against the schema.', 'ai' ) . '</li>' . | ||||||
| '<li>' . __( 'Click "Invoke Ability" to execute it and see the result.', 'ai' ) . '</li>' . | ||||||
|
Comment on lines
+647
to
+652
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another spacing removal |
||||||
| '</ol>', | ||||||
| ) | ||||||
| ); | ||||||
|
|
||||||
| $screen->set_help_sidebar( | ||||||
| '<p><strong>' . esc_html__( 'For more information:', 'ai' ) . '</strong></p>' . | ||||||
| '<p><a href="https://developer.wordpress.org/apis/abilities/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Abilities API Documentation', 'ai' ) . '</a></p>' | ||||||
| '<p><a href="https://developer.wordpress.org/apis/abilities/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Abilities API Documentation', 'ai' ) . '</a></p>' | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Spacing addition that can be removed |
||||||
| ); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { useState } from '@wordpress/element'; | ||
| import { Modal, Button, TextareaControl, Notice } from '@wordpress/components'; | ||
|
|
||
| /** | ||
| * @param {Object} props | ||
| * @param {Function} props.onClose Close/unmount callback. | ||
| * @param {Function} props.onSuccess Called with the generated JSON string on success. | ||
| * @param {string} props.abilitySlug Slug of the ability being tested. | ||
| * @param {Object} props.strings Localised string map from aiAbilityExplorer. | ||
| * @param {string} props.ajaxUrl WordPress admin-ajax URL. | ||
| * @param {string} props.nonce Nonce for ai_ability_explorer_generate_payload. | ||
| */ | ||
| export default function GeneratePayloadModal( { | ||
| onClose, | ||
| onSuccess, | ||
| abilitySlug, | ||
| strings, | ||
| ajaxUrl, | ||
| nonce, | ||
| } ) { | ||
| const [ command, setCommand ] = useState( '' ); | ||
| const [ isGenerating, setIsGenerating ] = useState( false ); | ||
| const [ error, setError ] = useState( '' ); | ||
|
|
||
| async function handleGenerate() { | ||
| if ( ! command.trim() ) { | ||
| setError( strings.generateEmptyCommandError ); | ||
| return; | ||
| } | ||
|
|
||
| setIsGenerating( true ); | ||
| setError( '' ); | ||
|
|
||
| const formData = new FormData(); | ||
| formData.append( 'action', 'ai_ability_explorer_generate_payload' ); | ||
| formData.append( 'nonce', nonce ); | ||
| formData.append( 'ability', abilitySlug ); | ||
| formData.append( 'command', command ); | ||
|
|
||
| try { | ||
| const response = await fetch( ajaxUrl, { | ||
| method: 'POST', | ||
| body: formData, | ||
| credentials: 'same-origin', | ||
| } ); | ||
| const data = await response.json(); | ||
|
|
||
| if ( data.success && data.data?.payload ) { | ||
| onSuccess( data.data.payload ); | ||
| onClose(); | ||
| } else { | ||
| setError( data.data?.message || strings.generateError ); | ||
| } | ||
| } catch { | ||
| setError( strings.generateError ); | ||
| } finally { | ||
| setIsGenerating( false ); | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <Modal | ||
| title={ strings.generateModalTitle } | ||
| onRequestClose={ onClose } | ||
| size="medium" | ||
| > | ||
| <TextareaControl | ||
| label={ strings.generateModalLabel } | ||
| value={ command } | ||
| onChange={ setCommand } | ||
| rows={ 4 } | ||
| placeholder={ strings.generateModalPlaceholder } | ||
| /> | ||
|
|
||
| <div className="ability-generate-modal-error"> | ||
| { error && ( | ||
| <Notice status="error" isDismissible={ false }> | ||
| { error } | ||
| </Notice> | ||
| ) } | ||
| </div> | ||
|
|
||
| <div className="ability-generate-modal-footer"> | ||
| <Button | ||
| variant="primary" | ||
| onClick={ handleGenerate } | ||
| disabled={ isGenerating } | ||
| isBusy={ isGenerating } | ||
| accessibleWhenDisabled | ||
| > | ||
| { isGenerating ? strings.generating : strings.generateBtn } | ||
| </Button> | ||
| <Button variant="tertiary" onClick={ onClose }> | ||
| { strings.cancelBtn } | ||
| </Button> | ||
| </div> | ||
| </Modal> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's quite a few of these spacing changes in this PR. Any particular reason for those? If not, reverting those keeps the diff here cleaner and easier to review