Skip to content

Commit 52e3c30

Browse files
committed
AI: Add the WordPress AI Client.
The WordPress AI Client is a provider-agnostic API for WordPress code to call generative AI models via a consistent interface. Plugins and Core can use it to provide AI driven features for users, while users maintain full autonomy in choosing which AI provider(s) they want to rely on and how they configure them. This changeset merges the technical foundation for the WordPress AI Client into Core. This foundation was originally implemented in the https://github.com/WordPress/wp-ai-client package, which will be sunset going forward. The underlying https://github.com/WordPress/php-ai-client package is bundled with this changeset and will remain a separate library maintained by the WordPress project, for WordPress Core and the PHP ecosystem. No AI providers are bundled out of the box. Without explicit configuration and explicit calling code, WordPress will not send prompts or data to any external service. Site owners will be able to install plugins to enable usage of specific AI providers, built on top of this foundation. This is the first changeset of two that are most relevant for the AI Client feature. The subsequent change will introduce a configuration screen for different AI providers, where users can install provider plugins, configure their credentials, and enable the canonical WordPress AI plugin. Together, this infrastructure and UI will enable the WordPress ecosystem to build AI features in a seamless and interoperable way. Original merge proposal: https://make.wordpress.org/core/2026/02/03/proposal-for-merging-wp-ai-client-into-wordpress-7-0/ Props jason_the_adams, flixos90, desrosj, dkotter, jorgefilipecosta, peterwilsoncc, johnbillion, jorbin, swissspidy, isotropic. See #64591. git-svn-id: https://develop.svn.wordpress.org/trunk@61700 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 8a82e67 commit 52e3c30

File tree

169 files changed

+22813
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

169 files changed

+22813
-0
lines changed

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
<exclude-pattern>/src/wp-includes/js/*</exclude-pattern>
7474
<exclude-pattern>/src/wp-includes/PHPMailer/*</exclude-pattern>
7575
<exclude-pattern>/src/wp-includes/Requests/*</exclude-pattern>
76+
<exclude-pattern>/src/wp-includes/php-ai-client/*</exclude-pattern>
7677
<exclude-pattern>/src/wp-includes/SimplePie/*</exclude-pattern>
7778
<exclude-pattern>/src/wp-includes/sodium_compat/*</exclude-pattern>
7879
<exclude-pattern>/src/wp-includes/Text/*</exclude-pattern>

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<directory suffix=".php">src/wp-includes/IXR</directory>
4848
<directory suffix=".php">src/wp-includes/PHPMailer</directory>
4949
<directory suffix=".php">src/wp-includes/Requests</directory>
50+
<directory suffix=".php">src/wp-includes/php-ai-client</directory>
5051
<directory suffix=".php">src/wp-includes/SimplePie</directory>
5152
<directory suffix=".php">src/wp-includes/sodium_compat</directory>
5253
<directory suffix=".php">src/wp-includes/Text</directory>

src/wp-includes/ai-client.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* WordPress AI Client API.
4+
*
5+
* @package WordPress
6+
* @subpackage AI
7+
* @since 7.0.0
8+
*/
9+
10+
use WordPress\AiClient\AiClient;
11+
12+
/**
13+
* Creates a new AI prompt builder using the default provider registry.
14+
*
15+
* This is the main entry point for generating AI content in WordPress. It returns
16+
* a fluent builder that can be used to configure and execute AI prompts.
17+
*
18+
* The prompt can be provided as a simple string for basic text prompts, or as more
19+
* complex types for advanced use cases like multi-modal content or conversation history.
20+
*
21+
* @since 7.0.0
22+
*
23+
* @param string|MessagePart|Message|array|list<string|MessagePart|array>|list<Message>|null $prompt Optional. Initial prompt content.
24+
* A string for simple text prompts,
25+
* a MessagePart or Message object for
26+
* structured content, an array for a
27+
* message array shape, or a list of
28+
* parts or messages for multi-turn
29+
* conversations. Default null.
30+
* @return WP_AI_Client_Prompt_Builder The prompt builder instance.
31+
*/
32+
function wp_ai_client_prompt( $prompt = null ) {
33+
return new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry(), $prompt );
34+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
<?php
2+
/**
3+
* WP AI Client: WP_AI_Client_Cache class
4+
*
5+
* @package WordPress
6+
* @subpackage AI
7+
* @since 7.0.0
8+
*/
9+
10+
use WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface;
11+
12+
/**
13+
* WordPress-specific PSR-16 cache adapter for the AI Client.
14+
*
15+
* Bridges PSR-16 cache operations to WordPress object cache functions,
16+
* enabling the AI client to leverage WordPress caching infrastructure.
17+
*
18+
* @since 7.0.0
19+
* @internal Intended only to wire up the PHP AI Client SDK to WordPress's caching system.
20+
* @access private
21+
*/
22+
class WP_AI_Client_Cache implements CacheInterface {
23+
24+
/**
25+
* Cache group used for all cache operations.
26+
*
27+
* @since 7.0.0
28+
* @var string
29+
*/
30+
private const CACHE_GROUP = 'wp_ai_client';
31+
32+
/**
33+
* Fetches a value from the cache.
34+
*
35+
* @since 7.0.0
36+
*
37+
* @param string $key The unique key of this item in the cache.
38+
* @param mixed $default_value Default value to return if the key does not exist.
39+
* @return mixed The value of the item from the cache, or $default_value in case of cache miss.
40+
*/
41+
public function get( $key, $default_value = null ) {
42+
$found = false;
43+
$value = wp_cache_get( $key, self::CACHE_GROUP, false, $found );
44+
45+
if ( ! $found ) {
46+
return $default_value;
47+
}
48+
49+
return $value;
50+
}
51+
52+
/**
53+
* Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time.
54+
*
55+
* @since 7.0.0
56+
*
57+
* @param string $key The key of the item to store.
58+
* @param mixed $value The value of the item to store, must be serializable.
59+
* @param null|int|DateInterval $ttl Optional. The TTL value of this item.
60+
* @return bool True on success and false on failure.
61+
*/
62+
public function set( $key, $value, $ttl = null ): bool {
63+
$expire = $this->ttl_to_seconds( $ttl );
64+
65+
return wp_cache_set( $key, $value, self::CACHE_GROUP, $expire );
66+
}
67+
68+
/**
69+
* Delete an item from the cache by its unique key.
70+
*
71+
* @since 7.0.0
72+
*
73+
* @param string $key The unique cache key of the item to delete.
74+
* @return bool True if the item was successfully removed. False if there was an error.
75+
*/
76+
public function delete( $key ): bool {
77+
return wp_cache_delete( $key, self::CACHE_GROUP );
78+
}
79+
80+
/**
81+
* Wipes clean the entire cache's keys.
82+
*
83+
* This method only clears the cache group used by this adapter. If the underlying
84+
* cache implementation does not support group flushing, this method returns false.
85+
*
86+
* @since 7.0.0
87+
*
88+
* @return bool True on success and false on failure.
89+
*/
90+
public function clear(): bool {
91+
if ( ! function_exists( 'wp_cache_supports' ) || ! wp_cache_supports( 'flush_group' ) ) {
92+
return false;
93+
}
94+
95+
return wp_cache_flush_group( self::CACHE_GROUP );
96+
}
97+
98+
/**
99+
* Obtains multiple cache items by their unique keys.
100+
*
101+
* @since 7.0.0
102+
*
103+
* @param iterable<string> $keys A list of keys that can be obtained in a single operation.
104+
* @param mixed $default_value Default value to return for keys that do not exist.
105+
* @return array<string, mixed> A list of key => value pairs.
106+
*/
107+
public function getMultiple( $keys, $default_value = null ) {
108+
/**
109+
* Keys array.
110+
*
111+
* @var array<string> $keys_array
112+
*/
113+
$keys_array = $this->iterable_to_array( $keys );
114+
$values = wp_cache_get_multiple( $keys_array, self::CACHE_GROUP );
115+
$result = array();
116+
117+
foreach ( $keys_array as $key ) {
118+
if ( false === $values[ $key ] ) {
119+
// Could be a stored false or a cache miss — disambiguate via get().
120+
$result[ $key ] = $this->get( $key, $default_value );
121+
} else {
122+
$result[ $key ] = $values[ $key ];
123+
}
124+
}
125+
126+
return $result;
127+
}
128+
129+
/**
130+
* Persists a set of key => value pairs in the cache, with an optional TTL.
131+
*
132+
* @since 7.0.0
133+
*
134+
* @param iterable<string, mixed> $values A list of key => value pairs for a multiple-set operation.
135+
* @param null|int|DateInterval $ttl Optional. The TTL value of this item.
136+
* @return bool True on success and false on failure.
137+
*/
138+
public function setMultiple( $values, $ttl = null ): bool {
139+
$values_array = $this->iterable_to_array( $values );
140+
$expire = $this->ttl_to_seconds( $ttl );
141+
$results = wp_cache_set_multiple( $values_array, self::CACHE_GROUP, $expire );
142+
143+
// Return true only if all operations succeeded.
144+
return ! in_array( false, $results, true );
145+
}
146+
147+
/**
148+
* Deletes multiple cache items in a single operation.
149+
*
150+
* @since 7.0.0
151+
*
152+
* @param iterable<string> $keys A list of string-based keys to be deleted.
153+
* @return bool True if the items were successfully removed. False if there was an error.
154+
*/
155+
public function deleteMultiple( $keys ): bool {
156+
$keys_array = $this->iterable_to_array( $keys );
157+
$results = wp_cache_delete_multiple( $keys_array, self::CACHE_GROUP );
158+
159+
// Return true only if all operations succeeded.
160+
return ! in_array( false, $results, true );
161+
}
162+
163+
/**
164+
* Determines whether an item is present in the cache.
165+
*
166+
* @since 7.0.0
167+
*
168+
* @param string $key The cache item key.
169+
* @return bool True if the item exists in the cache, false otherwise.
170+
*/
171+
public function has( $key ): bool {
172+
$found = false;
173+
wp_cache_get( $key, self::CACHE_GROUP, false, $found );
174+
175+
return (bool) $found;
176+
}
177+
178+
/**
179+
* Converts a PSR-16 TTL value to seconds for WordPress cache functions.
180+
*
181+
* @since 7.0.0
182+
*
183+
* @param null|int|DateInterval $ttl The TTL value.
184+
* @return int The TTL in seconds, or 0 for no expiration.
185+
*/
186+
private function ttl_to_seconds( $ttl ): int {
187+
if ( null === $ttl ) {
188+
return 0;
189+
}
190+
191+
if ( $ttl instanceof DateInterval ) {
192+
$now = new DateTime();
193+
$end = ( clone $now )->add( $ttl );
194+
195+
return $end->getTimestamp() - $now->getTimestamp();
196+
}
197+
198+
return max( 0, (int) $ttl );
199+
}
200+
201+
/**
202+
* Converts an iterable to an array.
203+
*
204+
* @since 7.0.0
205+
*
206+
* @param iterable<mixed> $items The iterable to convert.
207+
* @return array<mixed> The array.
208+
*/
209+
private function iterable_to_array( $items ): array {
210+
if ( is_array( $items ) ) {
211+
return $items;
212+
}
213+
214+
return iterator_to_array( $items );
215+
}
216+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
/**
3+
* WP AI Client: WP_AI_Client_Discovery_Strategy class
4+
*
5+
* @package WordPress
6+
* @subpackage AI
7+
* @since 7.0.0
8+
*/
9+
10+
use WordPress\AiClient\Providers\Http\Abstracts\AbstractClientDiscoveryStrategy;
11+
use WordPress\AiClientDependencies\Nyholm\Psr7\Factory\Psr17Factory;
12+
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
13+
14+
/**
15+
* Discovery strategy for WordPress HTTP client.
16+
*
17+
* Registers the WordPress HTTP client adapter with the HTTPlug discovery system
18+
* so the AI Client SDK can find and use it automatically.
19+
*
20+
* @since 7.0.0
21+
* @internal Intended only to register WordPress's HTTP client so that the PHP AI Client SDK can use it.
22+
* @access private
23+
*/
24+
class WP_AI_Client_Discovery_Strategy extends AbstractClientDiscoveryStrategy {
25+
26+
/**
27+
* Creates an instance of the WordPress HTTP client.
28+
*
29+
* @since 7.0.0
30+
*
31+
* @param Psr17Factory $psr17_factory The PSR-17 factory for creating HTTP messages.
32+
* @return ClientInterface The PSR-18 HTTP client.
33+
*/
34+
protected static function createClient( Psr17Factory $psr17_factory ): ClientInterface {
35+
return new WP_AI_Client_HTTP_Client( $psr17_factory, $psr17_factory );
36+
}
37+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
/**
3+
* WP AI Client: WP_AI_Client_Event_Dispatcher class
4+
*
5+
* @package WordPress
6+
* @subpackage AI
7+
* @since 7.0.0
8+
*/
9+
10+
use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;
11+
12+
/**
13+
* WordPress-specific PSR-14 event dispatcher for the AI Client.
14+
*
15+
* Bridges PSR-14 events to WordPress action hooks, enabling plugins to hook
16+
* into AI client lifecycle events.
17+
*
18+
* @since 7.0.0
19+
* @internal Intended only to wire up the PHP AI Client SDK to WordPress's hook system.
20+
* @access private
21+
*/
22+
class WP_AI_Client_Event_Dispatcher implements EventDispatcherInterface {
23+
24+
/**
25+
* Dispatches an event to WordPress action hooks.
26+
*
27+
* Converts the event class name to a WordPress action hook name and fires it.
28+
* For example, BeforeGenerateResultEvent becomes wp_ai_client_before_generate_result.
29+
*
30+
* @since 7.0.0
31+
*
32+
* @param object $event The event object to dispatch.
33+
* @return object The same event object, potentially modified by listeners.
34+
*/
35+
public function dispatch( object $event ): object {
36+
$event_name = $this->get_hook_name_portion_for_event( $event );
37+
38+
/**
39+
* Fires when an AI client event is dispatched.
40+
*
41+
* The dynamic portion of the hook name, `$event_name`, refers to the
42+
* snake_case version of the event class name, without the `_event` suffix.
43+
*
44+
* For example, an event class named `BeforeGenerateResultEvent` will fire the
45+
* `wp_ai_client_before_generate_result` action hook.
46+
*
47+
* In practice, the available action hook names are:
48+
*
49+
* - wp_ai_client_before_generate_result
50+
* - wp_ai_client_after_generate_result
51+
*
52+
* @since 7.0.0
53+
*
54+
* @param object $event The event object.
55+
*/
56+
do_action( "wp_ai_client_{$event_name}", $event );
57+
58+
return $event;
59+
}
60+
61+
/**
62+
* Converts an event object class name to a WordPress action hook name portion.
63+
*
64+
* @since 7.0.0
65+
*
66+
* @param object $event The event object.
67+
* @return string The hook name portion derived from the event class name.
68+
*/
69+
private function get_hook_name_portion_for_event( object $event ): string {
70+
$class_name = get_class( $event );
71+
$pos = strrpos( $class_name, '\\' );
72+
$short_name = false !== $pos ? substr( $class_name, $pos + 1 ) : $class_name;
73+
74+
// Convert PascalCase to snake_case.
75+
$snake_case = strtolower( (string) preg_replace( '/([a-z])([A-Z])/', '$1_$2', $short_name ) );
76+
77+
// Strip '_event' suffix if present.
78+
if ( str_ends_with( $snake_case, '_event' ) ) {
79+
$snake_case = (string) substr( $snake_case, 0, -6 );
80+
}
81+
82+
return $snake_case;
83+
}
84+
}

0 commit comments

Comments
 (0)