| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — AI Copilot settings + capability helpers. |
| 4 |
* |
| 5 |
* The Copilot no longer stores credentials of its own. WordPress 7.0 owns |
| 6 |
* provider credentials (Settings → Connectors) and model routing |
| 7 |
* (`wp_ai_client_prompt()`, which injects the configured key automatically). |
| 8 |
* These helpers therefore only carry the per-user "AI assistant" toggle |
| 9 |
* (`ai.enabled`) and expose the Core capability signals the shell uses to |
| 10 |
* decide whether to surface the assistant at all. Provider + model selection |
| 11 |
* is delegated entirely to the Core AI Client — nothing is persisted here. |
| 12 |
* |
| 13 |
* @package OpenStation |
| 14 |
*/ |
| 15 |
|
| 16 |
defined( 'ABSPATH' ) || exit; |
| 17 |
|
| 18 |
/** |
| 19 |
* Returns the AI settings block for a given user. |
| 20 |
* |
| 21 |
* `enabled` defaults to `false`: the assistant is opt-in, turned on from |
| 22 |
* OS Settings → Features (and only enable-able once a provider is configured). |
| 23 |
* Provider + model selection is left entirely to the Core AI Client. |
| 24 |
* |
| 25 |
* @param int $user_id |
| 26 |
* @return array{ enabled: bool } |
| 27 |
*/ |
| 28 |
function openstation_ai_get_settings( $user_id ) { |
| 29 |
$os = openstation_get_os_settings( (int) $user_id ); |
| 30 |
$ai = isset( $os['ai'] ) && is_array( $os['ai'] ) ? $os['ai'] : array(); |
| 31 |
return array( |
| 32 |
'enabled' => isset( $ai['enabled'] ) ? (bool) $ai['enabled'] : false, |
| 33 |
); |
| 34 |
} |
| 35 |
|
| 36 |
/** |
| 37 |
* Whether the Core AI primitives the Copilot depends on are present. |
| 38 |
* |
| 39 |
* The assistant is built on the AI Client (generation), the Connectors API |
| 40 |
* (credentials) and the Abilities API (tools, adopted in a follow-up). When |
| 41 |
* any of these is missing — e.g. WordPress < 7.0, or AI disabled site-wide |
| 42 |
* via `wp_supports_ai()` — the shell hides the assistant entirely. |
| 43 |
* |
| 44 |
* @return bool |
| 45 |
*/ |
| 46 |
function openstation_ai_is_available() { |
| 47 |
return function_exists( 'wp_ai_client_prompt' ) |
| 48 |
&& function_exists( 'wp_get_connectors' ) |
| 49 |
&& function_exists( 'wp_register_ability' ) |
| 50 |
&& function_exists( 'wp_supports_ai' ) |
| 51 |
&& wp_supports_ai(); |
| 52 |
} |
| 53 |
|
| 54 |
/** |
| 55 |
* Whether a text-generation provider is configured and usable. |
| 56 |
* |
| 57 |
* The baseline capability gate: a no-network, deterministic |
| 58 |
* `is_supported_for_text_generation()` probe against the AI Client registry |
| 59 |
* (which Core populates from the configured Connectors). This is what plain |
| 60 |
* text-generation features — e.g. comment scoring, which only needs structured |
| 61 |
* text output — gate on. The agentic assistant needs more; see |
| 62 |
* {@see openstation_ai_assistant_provider_configured()}. |
| 63 |
* |
| 64 |
* Credentials are supplied by Core from the configured Connector; no API request |
| 65 |
* is made. |
| 66 |
* |
| 67 |
* @return bool |
| 68 |
*/ |
| 69 |
function openstation_ai_provider_configured() { |
| 70 |
if ( ! openstation_ai_is_available() ) { |
| 71 |
return false; |
| 72 |
} |
| 73 |
return (bool) wp_ai_client_prompt( 'test' )->is_supported_for_text_generation(); |
| 74 |
} |
| 75 |
|
| 76 |
/** |
| 77 |
* Whether a provider that can actually run the agentic assistant is configured. |
| 78 |
* |
| 79 |
* Follows the AI Client feature-detection guidance: rather than probing a bare |
| 80 |
* builder, we configure it the way the assistant actually calls the client — |
| 81 |
* with a function declaration — before the (no-network, deterministic) |
| 82 |
* `is_supported_for_text_generation()` check. `ModelRequirements::fromPromptData()` |
| 83 |
* turns the attached declaration into a `functionDeclarations` requirement, so |
| 84 |
* the check passes only when an available model supports text generation *and* |
| 85 |
* function calling — the two capabilities the tool loop depends on. |
| 86 |
* |
| 87 |
* Falls back to the plain text-generation gate when the SDK's FunctionDeclaration |
| 88 |
* class isn't present (e.g. older WordPress). |
| 89 |
* |
| 90 |
* @return bool |
| 91 |
*/ |
| 92 |
function openstation_ai_assistant_provider_configured() { |
| 93 |
if ( ! openstation_ai_is_available() ) { |
| 94 |
return false; |
| 95 |
} |
| 96 |
|
| 97 |
$probe = openstation_ai_capability_probe_declaration(); |
| 98 |
if ( ! $probe ) { |
| 99 |
return openstation_ai_provider_configured(); |
| 100 |
} |
| 101 |
|
| 102 |
return (bool) wp_ai_client_prompt( 'test' ) |
| 103 |
->using_function_declarations( $probe ) |
| 104 |
->is_supported_for_text_generation(); |
| 105 |
} |
| 106 |
|
| 107 |
/** |
| 108 |
* Builds a throwaway function declaration used only for capability detection. |
| 109 |
* |
| 110 |
* Mirrors the shape of the Copilot's real tool declarations so the AI Client's |
| 111 |
* support check additionally requires function-calling capability. Returns null |
| 112 |
* when the SDK class isn't available, letting the caller fall back to a plain |
| 113 |
* text-generation check. |
| 114 |
* |
| 115 |
* @return object|null A `FunctionDeclaration`, or null. |
| 116 |
*/ |
| 117 |
function openstation_ai_capability_probe_declaration() { |
| 118 |
$class = '\WordPress\AiClient\Tools\DTO\FunctionDeclaration'; |
| 119 |
if ( ! class_exists( $class ) ) { |
| 120 |
return null; |
| 121 |
} |
| 122 |
try { |
| 123 |
return new $class( |
| 124 |
'capability_probe', |
| 125 |
'Feature-detection probe; never invoked.', |
| 126 |
null |
| 127 |
); |
| 128 |
} catch ( \Throwable $e ) { |
| 129 |
return null; |
| 130 |
} |
| 131 |
} |
| 132 |
|
| 133 |
/** |
| 134 |
* Whether the AI assistant is active for a given user. |
| 135 |
* |
| 136 |
* This is purely the per-user toggle (default off, opt-in). Availability of the Core |
| 137 |
* APIs and whether a provider key is set are separate, orthogonal checks |
| 138 |
* ({@see openstation_ai_is_available()} / {@see openstation_ai_provider_configured()}) |
| 139 |
* so callers can distinguish "user turned it off" from "not set up yet". |
| 140 |
* |
| 141 |
* @param int $user_id |
| 142 |
* @return bool |
| 143 |
*/ |
| 144 |
function openstation_ai_is_enabled( $user_id ) { |
| 145 |
$ai = openstation_ai_get_settings( (int) $user_id ); |
| 146 |
return ! empty( $ai['enabled'] ); |
| 147 |
} |
| 148 |
|
| 149 |
/** |
| 150 |
* Builds the `aiAssistant` shell-config payload for a user. |
| 151 |
* |
| 152 |
* The client uses this to decide whether to surface the Cmd+K assistant and |
| 153 |
* its admin-bar icon at all (`available`) and the user's own on/off toggle |
| 154 |
* (`enabled`). Two capability gates are reported separately: |
| 155 |
* |
| 156 |
* - `assistantProviderConfigured` — a provider that supports text generation |
| 157 |
* *and* function calling (what the agentic assistant needs). Gates the Cmd+K |
| 158 |
* assistant, its admin-bar icon, and the "AI assistant" toggle in Features. |
| 159 |
* - `providerConfigured` — the baseline text-generation gate. Comment scoring |
| 160 |
* (which only needs text output) gates on this; the client uses it for the |
| 161 |
* "Score new comments with AI" mirror. |
| 162 |
* |
| 163 |
* Provider + model selection is delegated to the Core AI Client, so there is no |
| 164 |
* per-user preference to carry here. |
| 165 |
* |
| 166 |
* @param int|null $user_id Defaults to the current user. |
| 167 |
* @return array{ available: bool, providerConfigured: bool, assistantProviderConfigured: bool, enabled: bool, connectorsUrl: string } |
| 168 |
*/ |
| 169 |
function openstation_ai_assistant_config( $user_id = null ) { |
| 170 |
$user_id = null === $user_id ? get_current_user_id() : (int) $user_id; |
| 171 |
|
| 172 |
$connectors_url = admin_url( 'options-connectors.php' ); |
| 173 |
|
| 174 |
if ( ! openstation_ai_is_available() ) { |
| 175 |
return array( |
| 176 |
'available' => false, |
| 177 |
'providerConfigured' => false, |
| 178 |
'assistantProviderConfigured' => false, |
| 179 |
'enabled' => false, |
| 180 |
'connectorsUrl' => $connectors_url, |
| 181 |
); |
| 182 |
} |
| 183 |
|
| 184 |
$ai = openstation_ai_get_settings( $user_id ); |
| 185 |
|
| 186 |
return array( |
| 187 |
'available' => true, |
| 188 |
'providerConfigured' => openstation_ai_provider_configured(), |
| 189 |
'assistantProviderConfigured' => openstation_ai_assistant_provider_configured(), |
| 190 |
'enabled' => (bool) $ai['enabled'], |
| 191 |
'connectorsUrl' => $connectors_url, |
| 192 |
); |
| 193 |
} |
| 194 |
|
| 195 |
/** |
| 196 |
* REST: GET `desktop-mode/v1/ai/status`. |
| 197 |
* |
| 198 |
* Returns the current {@see openstation_ai_assistant_config()} so the shell |
| 199 |
* can re-check provider availability without a page reload — e.g. after the |
| 200 |
* user configures an AI provider in Settings → Connectors. |
| 201 |
*/ |
| 202 |
function openstation_register_ai_status_rest_route() { |
| 203 |
register_rest_route( |
| 204 |
'desktop-mode/v1', |
| 205 |
'/ai/status', |
| 206 |
array( |
| 207 |
'methods' => WP_REST_Server::READABLE, |
| 208 |
'callback' => 'openstation_rest_ai_status', |
| 209 |
'permission_callback' => static function () { |
| 210 |
return is_user_logged_in() && current_user_can( 'read' ); |
| 211 |
}, |
| 212 |
) |
| 213 |
); |
| 214 |
} |
| 215 |
add_action( 'rest_api_init', 'openstation_register_ai_status_rest_route' ); |
| 216 |
|
| 217 |
/** |
| 218 |
* REST handler for the AI status probe. |
| 219 |
* |
| 220 |
* @return WP_REST_Response |
| 221 |
*/ |
| 222 |
function openstation_rest_ai_status() { |
| 223 |
return rest_ensure_response( openstation_ai_assistant_config() ); |
| 224 |
} |
| 225 |
|