Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions includes/Experiments/Abilities_Explorer/Abilities_Explorer.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

Copy link
Copy Markdown
Collaborator

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

/**
* Abilities Explorer Experiment
*
Expand All @@ -9,7 +10,7 @@
* @since 0.2.0
*/

declare( strict_types=1 );
declare(strict_types=1);

namespace WordPress\AI\Experiments\Abilities_Explorer;

Expand All @@ -30,6 +31,7 @@
* @since 0.2.0
*/
class Abilities_Explorer extends Abstract_Feature {

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -79,17 +81,26 @@ public function enqueue_assets( string $hook_suffix ): void {
'abilities_explorer',
'AbilityExplorer',
array(
'enabled' => $this->is_enabled(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ai_ability_explorer_invoke' ),
'strings' => array(
'invoking' => esc_html__( 'Invoking ability...', 'ai' ),
'success' => esc_html__( 'Success!', 'ai' ),
'error' => esc_html__( 'Error', 'ai' ),
'invalidJson' => esc_html__( 'Invalid JSON input', 'ai' ),
'confirmInvoke' => esc_html__( 'Are you sure you want to invoke this ability?', 'ai' ),
'copySuccess' => esc_html__( 'Copied!', 'ai' ),
'copyError' => esc_html__( 'Failed to copy', 'ai' ),
'enabled' => $this->is_enabled(),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ai_ability_explorer_invoke' ),
'generateNonce' => wp_create_nonce( 'ai_ability_explorer_generate_payload' ),
'strings' => array(
'invoking' => esc_html__( 'Invoking ability...', 'ai' ),
'success' => esc_html__( 'Success!', 'ai' ),
'error' => esc_html__( 'Error', 'ai' ),
'invalidJson' => esc_html__( 'Invalid JSON input', 'ai' ),
'confirmInvoke' => esc_html__( 'Are you sure you want to invoke this ability?', 'ai' ),
'copySuccess' => esc_html__( 'Copied!', 'ai' ),
'copyError' => esc_html__( 'Failed to copy', 'ai' ),
'generating' => esc_html__( 'Generating...', 'ai' ),
'generateModalTitle' => esc_html__( 'Generate Payload with AI', 'ai' ),
'generateModalLabel' => esc_html__( 'Describe what you want to test in natural language:', 'ai' ),
'generateModalPlaceholder' => esc_html__( 'e.g. Query only the site URL information', 'ai' ),
'generateBtn' => esc_html__( 'Generate', 'ai' ),
'cancelBtn' => esc_html__( 'Cancel', 'ai' ),
'generateEmptyCommandError' => esc_html__( 'Please enter a command.', 'ai' ),
'generateError' => esc_html__( 'An error occurred while generating the payload. Please try again.', 'ai' ),
),
)
);
Expand Down
154 changes: 136 additions & 18 deletions includes/Experiments/Abilities_Explorer/Admin_Page.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/**
* Admin Page Class
*
Expand All @@ -8,7 +9,7 @@
* @since 0.2.0
*/

declare( strict_types=1 );
declare(strict_types=1);

namespace WordPress\AI\Experiments\Abilities_Explorer;

Expand All @@ -25,6 +26,7 @@
*/
class Admin_Page {


/**
* Initialize admin functionality.
*
Expand All @@ -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' ) );
}

/**
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was spacing changed here?

</p>
</div>
<?php endif; ?>
Expand All @@ -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>
Expand Down Expand Up @@ -483,6 +491,116 @@ public function ajax_invoke_ability(): void {
}
}

/**
* AJAX handler for AI-assisted payload generation.
*
* @since 1.0.1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All @since statements should be set to x.x.x

*/
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' ) ) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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' ),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'message' => __( 'No AI provider available. Please connect one in the plugin settings.', 'ai' ),
'message' => __( 'Generation failed. Please ensure you have a connected provider that supports text generation.', 'ai' ),

)
);
}

$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.
*
Expand All @@ -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>' .

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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>',
)
);
Expand All @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another spacing removal

'</ul>',
)
);
Expand All @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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>'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing addition that can be removed

);
}
}
102 changes: 102 additions & 0 deletions src/experiments/abilities-explorer/components/GeneratePayloadModal.jsx
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>
);
}
Loading
Loading