PluginProbe
AI / trunk
AI vtrunk
1.3.0 1.2.0 1.1.0 1.0.2 1.0.1 1.0.0 0.9.0 trunk 0.1.1 0.2.0 0.2.1 0.3.0 0.3.1 0.4.0 0.4.1 0.5.0 0.6.0 0.7.0 0.8.0
ai / includes / Abstracts / Abstract_Ability.php

Abstract_Ability.php in AI trunk, at includes/Abstracts/Abstract_Ability.php

428 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abstract Ability base class.
4 *
5 * @package WordPress\AI\Abstracts
6 */
7
8 declare( strict_types=1 );
9
10 namespace WordPress\AI\Abstracts;
11
12 use ReflectionClass;
13 use WP_Ability;
14 use WP_Error;
15
16 use WordPress\AiClient\AiClient;
17
18 use function WordPress\AI\format_guidelines_for_prompt;
19 use function WordPress\AI\get_feature_developer_model_config;
20 use function WordPress\AI\get_preferred_models_for_text_generation;
21
22 /**
23 * Base implementation for a WordPress Ability.
24 *
25 * @since 0.1.0
26 */
27 abstract class Abstract_Ability extends WP_Ability {
28
29 /**
30 * Constructor.
31 *
32 * @since 0.1.0
33 *
34 * @param string $name The name of the ability.
35 * @param array<string,mixed> $properties The properties of the ability. Must include `label`.
36 */
37 public function __construct( string $name, array $properties = array() ) {
38 parent::__construct(
39 $name,
40 array(
41 'label' => $properties['label'] ?? '',
42 'description' => $properties['description'] ?? '',
43 'category' => $this->category(),
44 'input_schema' => $this->input_schema(),
45 'output_schema' => $this->output_schema(),
46 'execute_callback' => array( $this, 'execute_callback' ),
47 'permission_callback' => array( $this, 'permission_callback' ),
48 'meta' => $this->meta(),
49 )
50 );
51 }
52
53 /**
54 * Returns the category of the ability.
55 *
56 * @since 0.1.0
57 *
58 * @return string The category of the ability.
59 */
60 protected function category(): string {
61 return WPAI_DEFAULT_ABILITY_CATEGORY;
62 }
63
64 /**
65 * Returns the input schema of the ability.
66 *
67 * @since 0.1.0
68 *
69 * @return array<string, mixed> The input schema of the ability.
70 */
71 abstract protected function input_schema(): array;
72
73 /**
74 * Returns the output schema of the ability.
75 *
76 * @since 0.1.0
77 *
78 * @return array<string, mixed> The output schema of the ability.
79 */
80 abstract protected function output_schema(): array;
81
82 /**
83 * Executes the ability with the given input arguments.
84 *
85 * @since 0.1.0
86 *
87 * @param mixed $input The input arguments to the ability.
88 * @return mixed|\WP_Error The result of the ability execution, or a WP_Error on failure.
89 */
90 abstract protected function execute_callback( $input );
91
92 /**
93 * Checks whether the current user has permission to execute the ability with the given input arguments.
94 *
95 * @since 0.1.0
96 *
97 * @param mixed $input The input arguments to the ability.
98 * @return bool|\WP_Error True if the user has permission, WP_Error otherwise.
99 */
100 abstract protected function permission_callback( $input );
101
102 /**
103 * Returns the meta of the ability.
104 *
105 * @since 0.1.0
106 *
107 * @return array<string, mixed> The meta of the ability.
108 */
109 abstract protected function meta(): array;
110
111 /**
112 * Returns the guideline categories this ability uses.
113 *
114 * Override in subclasses to opt into guidelines.
115 * Return an empty array to skip guidelines (default).
116 *
117 * Valid categories: 'site', 'copy', 'images', 'additional'.
118 *
119 * @since 0.8.0
120 *
121 * @return list<string> Guideline category slugs.
122 */
123 protected function guideline_categories(): array {
124 return array();
125 }
126
127 /**
128 * Returns formatted guidelines for prompt injection.
129 *
130 * Uses guideline_categories() to determine which categories to include.
131 * Unsupported categories are silently dropped.
132 * Returns empty string when guidelines are unavailable or no categories declared.
133 *
134 * @since 0.8.0
135 *
136 * @param string|null $block_name Optional block name for block-specific guidelines.
137 * @return string Formatted guidelines XML string, or empty string.
138 */
139 protected function get_guidelines_for_prompt( ?string $block_name = null ): string {
140 $categories = array_values(
141 array_intersect(
142 $this->guideline_categories(),
143 array( 'site', 'copy', 'images', 'additional' )
144 )
145 );
146 if ( empty( $categories ) ) {
147 return '';
148 }
149 return format_guidelines_for_prompt( $categories, $block_name );
150 }
151
152 /**
153 * Gets the system instruction for the feature.
154 *
155 * When guideline_categories() returns a non-empty array and guidelines are
156 * available, automatically appends them to the system instruction.
157 *
158 * Supports a reserved `block_name` key in `$data` for block-specific guidelines.
159 *
160 * @since 0.1.0
161 *
162 * @param string|null $filename Optional. Explicit filename to load. If not provided,
163 * attempts to load `system-instruction.php` or `prompt.php`.
164 * @param array<string, mixed> $data Optional. Data to expose to the system instruction file.
165 * This data will be extracted as variables available in the file scope.
166 * @return string The system instruction for the feature.
167 */
168 public function get_system_instruction( ?string $filename = null, array $data = array() ): string {
169 $block_name = null;
170 if ( isset( $data['block_name'] ) && is_string( $data['block_name'] ) ) {
171 $block_name = $data['block_name'];
172 unset( $data['block_name'] );
173 }
174
175 $instruction = $this->load_system_instruction_from_file( $filename, $data );
176
177 if ( '' !== $instruction && ! empty( $this->guideline_categories() ) ) {
178 $guidelines = $this->get_guidelines_for_prompt( $block_name );
179
180 if ( $guidelines ) {
181 $instruction .= "\n\n" . 'The following guidelines represent the site&#039;s editorial standards. Apply them where relevant. Do not fabricate content to satisfy guidelines. If guidelines conflict with the input, prioritize accuracy.';
182 $instruction .= "\n\n" . $guidelines;
183 }
184 }
185
186 /**
187 * Filters the system instruction for an ability.
188 *
189 * @since 0.7.0
190 *
191 * @param string $instruction The system instruction text.
192 * @param string $name The name of the ability.
193 * @param array $data The data passed to the system instruction file.
194 */
195 $instruction = apply_filters( 'wpai_system_instruction', $instruction, $this->get_name(), $data );
196
197 /**
198 * Filters the system instruction for a specific ability.
199 *
200 * The dynamic portion of the hook name, `$slug`, refers to the ability slug
201 * derived from its name (e.g. `ai/title-generation` becomes `title_generation`).
202 *
203 * This scoped filter runs after the global `wpai_system_instruction` filter,
204 * allowing developers to target a single ability without inspecting the name.
205 *
206 * @since 1.3.0
207 *
208 * @param string $instruction The system instruction text.
209 * @param array $data The data passed to the system instruction file.
210 */
211 return apply_filters( "wpai_{$this->get_ability_slug()}_system_instruction", $instruction, $data );
212 }
213
214 /**
215 * Returns the hook-safe slug for this ability.
216 *
217 * Derived from the ability name by stripping the `ai/` namespace prefix and
218 * converting hyphens to underscores. For example, `ai/title-generation`
219 * becomes `title_generation`. Used to build per-ability filter hook names.
220 *
221 * @since 1.3.0
222 *
223 * @return string The hook-safe ability slug.
224 */
225 protected function get_ability_slug(): string {
226 $name = (string) $this->get_name();
227 $name = preg_replace( '#^ai/#', '', $name );
228
229 return str_replace( '-', '_', (string) $name );
230 }
231
232 /**
233 * Loads system instruction from a PHP file in the feature's directory.
234 *
235 * PHP files should return a string directly, e.g.:
236 * ```php
237 * <?php
238 * return 'Your system instruction text here...';
239 * ```
240 *
241 * If data is provided, it will be extracted as variables available in the file scope.
242 * For example, if you pass `array( 'length' => 'short' )`, the variable `$length`
243 * will be available in the system instruction file.
244 *
245 * @since 0.1.0
246 *
247 * @param string|null $filename Optional. Explicit filename to load. If not provided,
248 * attempts to load `system-instruction.php`.
249 * @param array<string, mixed> $data Optional. Data to expose to the system instruction file.
250 * This data will be extracted as variables available in the file scope.
251 * @return string The contents of the file, or empty string if file not found.
252 */
253 protected function load_system_instruction_from_file( ?string $filename = null, array $data = array() ): string {
254 // Get the feature's directory using reflection.
255 $reflection = new ReflectionClass( $this );
256 $file_name = $reflection->getFileName();
257
258 if ( ! $file_name ) {
259 return '';
260 }
261
262 $feature_dir = dirname( $file_name );
263
264 // Extract data into variables for use in the included file.
265 if ( ! empty( $data ) ) {
266 extract( $data, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
267 }
268
269 // If explicit filename provided, use it.
270 if ( null !== $filename ) {
271 $file_path = trailingslashit( $feature_dir ) . $filename;
272
273 if ( file_exists( $file_path ) && is_readable( $file_path ) ) {
274 // PHP files should return a string directly.
275 $content = require $file_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable
276
277 return is_string( $content ) ? $content : '';
278 }
279
280 return '';
281 }
282
283 // Automatic detection if no filename provided.
284 $file_path = trailingslashit( $feature_dir ) . 'system-instruction.php';
285
286 if ( file_exists( $file_path ) && is_readable( $file_path ) ) {
287 // PHP files should return a string directly.
288 $content = require $file_path; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable
289
290 return is_string( $content ) ? $content : '';
291 }
292
293 return '';
294 }
295
296 /**
297 * Ensures the prompt builder can run text generation.
298 *
299 * @since 0.7.0
300 *
301 * @param \WP_AI_Client_Prompt_Builder $prompt_builder The configured prompt builder.
302 * @param string $message User-visible error message.
303 * @return \WP_AI_Client_Prompt_Builder|\WP_Error The prompt builder, or a WP_Error on failure.
304 */
305 protected function ensure_text_generation_supported( $prompt_builder, string $message ) {
306 if ( ! $prompt_builder->is_supported_for_text_generation() ) {
307 return new WP_Error( 'unsupported_model', $message );
308 }
309
310 return $prompt_builder;
311 }
312
313 /**
314 * Ensures the prompt builder can run image generation.
315 *
316 * @since 0.7.0
317 *
318 * @param \WP_AI_Client_Prompt_Builder $prompt_builder The configured prompt builder.
319 * @param string $message User-visible error message.
320 * @return \WP_AI_Client_Prompt_Builder|\WP_Error The prompt builder, or a WP_Error on failure.
321 */
322 protected function ensure_image_generation_supported( $prompt_builder, string $message ) {
323 if ( ! $prompt_builder->is_supported_for_image_generation() ) {
324 return new WP_Error( 'unsupported_model', $message );
325 }
326
327 return $prompt_builder;
328 }
329
330 /**
331 * Sets the provider and model preference for a prompt builder based on developer mode settings.
332 *
333 * Reads the developer-configured provider/model for the given feature class and applies it
334 * to the prompt builder. Falls back to the supplied model preference list when no override
335 * is saved.
336 *
337 * @since 0.9.0
338 *
339 * @param \WP_AI_Client_Prompt_Builder $prompt_builder The prompt builder.
340 * @param class-string<\WordPress\AI\Contracts\Feature> $feature_class The feature class to read settings from.
341 * @param array<int, array{string, string}> $fallback_models The default models to use when no override is set.
342 * @return \WP_AI_Client_Prompt_Builder The prompt builder.
343 */
344 protected function set_provider_model_preference( \WP_AI_Client_Prompt_Builder $prompt_builder, string $feature_class, array $fallback_models = array() ): \WP_AI_Client_Prompt_Builder {
345 $config = get_feature_developer_model_config( $feature_class::get_id() );
346 $provider = $config['provider'];
347 $model = $config['model'];
348
349 if ( $provider && $model ) {
350 $prompt_builder->using_model(
351 AiClient::defaultRegistry()->getProviderModel( $provider, $model )
352 );
353 } else {
354 if ( $provider ) {
355 $prompt_builder->using_provider( $provider );
356 }
357
358 if ( empty( $fallback_models ) ) {
359 $fallback_models = get_preferred_models_for_text_generation();
360 }
361
362 $prompt_builder->using_model_preference( ...$fallback_models );
363 }
364
365 return $prompt_builder;
366 }
367
368 /**
369 * Filters the assembled user prompt.
370 *
371 * @since 1.3.0
372 *
373 * @param string $prompt The prompt string.
374 * @param mixed ...$filter_args Additional arguments to pass to the filter.
375 * @return string The filtered prompt string.
376 */
377 protected function filter_prompt( string $prompt, ...$filter_args ): string {
378 /**
379 * Filters the assembled user prompt for the ability.
380 *
381 * @since 1.3.0
382 *
383 * @param string $prompt The assembled prompt string.
384 * @param mixed ...$filter_args Additional arguments to pass to the filter.
385 */
386 return (string) apply_filters( "wpai_{$this->get_ability_slug()}_prompt", $prompt, ...$filter_args );
387 }
388
389 /**
390 * Configures a prompt builder with model preferences and applies the builder filter.
391 *
392 * @since 1.3.0
393 *
394 * @param \WP_AI_Client_Prompt_Builder $prompt_builder The configured prompt builder.
395 * @param class-string<\WordPress\AI\Contracts\Feature>|null $feature_class The feature class to read settings from, if any.
396 * @param array<int, array{string, string}> $fallback_models Optional fallback models for the developer override.
397 * @param mixed ...$filter_args Additional arguments to pass to the builder filter.
398 * @return \WP_AI_Client_Prompt_Builder The prompt builder.
399 */
400 protected function filter_prompt_builder( \WP_AI_Client_Prompt_Builder $prompt_builder, ?string $feature_class = null, array $fallback_models = array(), ...$filter_args ): \WP_AI_Client_Prompt_Builder {
401 if ( $feature_class ) {
402 $prompt_builder = $this->set_provider_model_preference( $prompt_builder, $feature_class, $fallback_models );
403 } elseif ( ! empty( $fallback_models ) ) {
404 $prompt_builder->using_model_preference( ...$fallback_models );
405 }
406
407 /**
408 * Filters the configured prompt builder for the ability.
409 *
410 * Runs after the model preference is applied and before generation
411 * support is verified. Extend the builder rather than replacing it, and
412 * always return a WP_AI_Client_Prompt_Builder.
413 *
414 * @since 1.3.0
415 *
416 * @param \WP_AI_Client_Prompt_Builder $prompt_builder The configured prompt builder.
417 * @param mixed ...$filter_args Additional context arguments.
418 */
419 $filtered_prompt_builder = apply_filters( "wpai_{$this->get_ability_slug()}_prompt_builder", $prompt_builder, ...$filter_args );
420
421 if ( ! $filtered_prompt_builder instanceof \WP_AI_Client_Prompt_Builder ) {
422 return $prompt_builder;
423 }
424
425 return $filtered_prompt_builder;
426 }
427 }
428