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
22 changes: 22 additions & 0 deletions includes/Abilities/Utilities/Posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ private function register_get_post_details_ability(): void {
$details['excerpt'] = $post->post_excerpt;
}

/**
* Filters the post details returned by the get-post-details ability.
*
* @since 0.5.0
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's replace all @since statements with x.x.x as we're not sure of the actual version until we prep a release (and we have a step in our release process to find and replace all of those)

*
* @param array<string, string> $details The post details.
* @param int $post_id The post ID.
* @param array<string> $fields The requested fields.
*/
$details = apply_filters( 'ai_experiments_get_post_details', $details, $post_id, $fields );

// Return the post details.
return $details;
},
Expand Down Expand Up @@ -297,6 +308,17 @@ private function register_get_terms_ability(): void {
);
}

/**
* Filters the terms returned by the get-post-terms ability.
*
* @since 0.5.0
*
* @param array<\WP_Term> $terms The terms assigned to the post.
* @param int $post_id The post ID.
* @param array<string> $allowed_taxonomies The allowed taxonomy names.
*/
$terms = apply_filters( 'ai_experiments_get_post_terms', $terms, $post_id, $allowed_taxonomies );

return $terms;
},
'permission_callback' => array( $this, 'permission_callback' ),
Expand Down
60 changes: 58 additions & 2 deletions includes/Abstracts/Abstract_Ability.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,58 @@ public function __construct( string $name, array $properties = array() ) {
'category' => $this->category(),
'input_schema' => $this->input_schema(),
'output_schema' => $this->output_schema(),
'execute_callback' => array( $this, 'execute_callback' ),
'execute_callback' => array( $this, 'filtered_execute_callback' ),
'permission_callback' => array( $this, 'permission_callback' ),
'meta' => $this->meta(),
)
);
}

/**
* Wraps execute_callback to apply result filters.
*
* @since 0.5.0
*
* @param mixed $input The input arguments to the ability.
* @return mixed|\WP_Error The filtered result of the ability execution, or a WP_Error on failure.
*/
public function filtered_execute_callback( $input ) {
$result = $this->execute_callback( $input );

// Don't filter WP_Error results.
if ( is_wp_error( $result ) ) {
return $result;
}

$name = $this->get_name();

/**
* Filters the result of any ability execution.
*
* @since 0.5.0
*
* @param mixed $result The result of the ability execution.
* @param string $name The name of the ability.
* @param mixed $input The input arguments to the ability.
*/
$result = apply_filters( 'ai_experiments_ability_result', $result, $name, $input );

/**
* Filters the result of a specific ability execution.
*
* The dynamic portion of the hook name, `$name`, refers to the ability name
* (e.g., 'ai/title-generation', 'ai/summarization').
*
* @since 0.5.0
*
* @param mixed $result The result of the ability execution.
* @param mixed $input The input arguments to the ability.
*/
$result = apply_filters( "ai_experiments_ability_result_{$name}", $result, $input );

return $result;
}

/**
* Returns the category of the ability.
*
Expand Down Expand Up @@ -125,7 +170,18 @@ abstract protected function meta(): array;
* @return string The system instruction for the feature.
*/
public function get_system_instruction( ?string $filename = null, array $data = array() ): string {
return $this->load_system_instruction_from_file( $filename, $data );
$instruction = $this->load_system_instruction_from_file( $filename, $data );

/**
* Filters the system instruction for an ability.
*
* @since 0.5.0
*
* @param string $instruction The system instruction text.
* @param string $name The name of the ability.
* @param array $data The data passed to the system instruction file.
*/
return apply_filters( 'ai_experiments_system_instruction', $instruction, $this->get_name(), $data );
}

/**
Expand Down
180 changes: 180 additions & 0 deletions tests/Integration/Includes/Abstracts/Abstract_AbilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -446,4 +446,184 @@ public function test_get_system_instruction_with_empty_data_array() {
}
}
}

/**
* Test that ai_experiments_system_instruction filter modifies system instructions.
*
* @since 0.5.0
*/
public function test_system_instruction_filter() {
$experiment = new Test_Ability_Experiment();
$ability = new Test_Ability(
'test-ability',
array(
'label' => $experiment->get_label(),
'description' => $experiment->get_description(),
)
);

$filter_callback = function ( $instruction, $name, $data ) {
return $instruction . ' Appended by filter.';
};

add_filter( 'ai_experiments_system_instruction', $filter_callback, 10, 3 );

$result = $ability->get_system_instruction();

remove_filter( 'ai_experiments_system_instruction', $filter_callback, 10 );

$this->assertStringContainsString( 'Appended by filter.', $result, 'System instruction filter should modify the instruction' );
}

/**
* Test that ai_experiments_system_instruction filter receives the correct ability name.
*
* @since 0.5.0
*/
public function test_system_instruction_filter_receives_ability_name() {
$experiment = new Test_Ability_Experiment();
$ability = new Test_Ability(
'test-ability',
array(
'label' => $experiment->get_label(),
'description' => $experiment->get_description(),
)
);

$captured_name = null;
$filter_callback = function ( $instruction, $name ) use ( &$captured_name ) {
$captured_name = $name;
return $instruction;
};

add_filter( 'ai_experiments_system_instruction', $filter_callback, 10, 2 );

$ability->get_system_instruction();

remove_filter( 'ai_experiments_system_instruction', $filter_callback, 10 );

$this->assertSame( 'test-ability', $captured_name, 'Filter should receive the ability name' );
}

/**
* Test that ai_experiments_ability_result filter modifies ability results.
*
* @since 0.5.0
*/
public function test_ability_result_filter() {
$experiment = new Test_Ability_Experiment();
$ability = new Test_Ability(
'test-ability',
array(
'label' => $experiment->get_label(),
'description' => $experiment->get_description(),
)
);

$filter_callback = function ( $result, $name, $input ) {
$result['filtered'] = true;
return $result;
};

add_filter( 'ai_experiments_ability_result', $filter_callback, 10, 3 );

$result = $ability->filtered_execute_callback( array() );

remove_filter( 'ai_experiments_ability_result', $filter_callback, 10 );

$this->assertArrayHasKey( 'filtered', $result, 'Result filter should add key to result' );
$this->assertTrue( $result['filtered'], 'Result filter should set filtered to true' );
}

/**
* Test that the dynamic ability result filter works.
*
* @since 0.5.0
*/
public function test_ability_result_dynamic_filter() {
$experiment = new Test_Ability_Experiment();
$ability = new Test_Ability(
'test-ability',
array(
'label' => $experiment->get_label(),
'description' => $experiment->get_description(),
)
);

$filter_callback = function ( $result, $input ) {
$result['specific'] = true;
return $result;
};

add_filter( 'ai_experiments_ability_result_test-ability', $filter_callback, 10, 2 );

$result = $ability->filtered_execute_callback( array() );

remove_filter( 'ai_experiments_ability_result_test-ability', $filter_callback, 10 );

$this->assertArrayHasKey( 'specific', $result, 'Dynamic result filter should add key to result' );
$this->assertTrue( $result['specific'], 'Dynamic result filter should set specific to true' );
}

/**
* Test that WP_Error results are not filtered.
*
* @since 0.5.0
*/
public function test_wp_error_not_filtered() {
$experiment = new Test_Ability_Experiment();
$ability = new Test_Error_Ability(
'test-error-ability',
array(
'label' => $experiment->get_label(),
'description' => $experiment->get_description(),
)
);

$filter_called = false;
$filter_callback = function ( $result ) use ( &$filter_called ) {
$filter_called = true;
return $result;
};

add_filter( 'ai_experiments_ability_result', $filter_callback, 10, 1 );

$result = $ability->filtered_execute_callback( array() );

remove_filter( 'ai_experiments_ability_result', $filter_callback, 10 );

$this->assertTrue( is_wp_error( $result ), 'WP_Error should be returned as-is' );
$this->assertFalse( $filter_called, 'Result filter should not be called for WP_Error' );
}
}

/**
* Test ability that returns WP_Error for testing error bypass.
*
* @since 0.5.0
*/
class Test_Error_Ability extends Abstract_Ability {
protected function category(): string {
return 'test-category';
}

protected function input_schema(): array {
return array( 'type' => 'object', 'properties' => array() );
}

protected function output_schema(): array {
return array( 'type' => 'object', 'properties' => array() );
}

protected function execute_callback( $input ) {
return new \WP_Error( 'test_error', 'Test error message' );
}

protected function permission_callback( $input ) {
return true;
}

protected function meta(): array {
return array();
}
}
Loading