PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.0
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / includes / ai-copilot / tools-registry.php

tools-registry.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.0, at includes/ai-copilot/tools-registry.php

279 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 defined( 'ABSPATH' ) || exit;
3 /**
4 * Desktop Mode — AI tool registration API.
5 *
6 * Third-party plugins register server-side tools the AI Copilot can
7 * invoke during a `/ai/search` call. Mirrors every other
8 * registration API in this plugin (`desktop_mode_register_window`,
9 * `desktop_mode_register_wallpaper`, `desktop_mode_register_widget`).
10 *
11 * Tools registered here are *PHP-dispatched* — the tool's `handler`
12 * runs on the server, returns a JSON-serialisable array, and the
13 * result is fed straight back to the OpenAI agent loop. This is the
14 * right home for integrations that are inherently server-side:
15 * site-health checks, WooCommerce lookups, WP-CLI wrappers, any
16 * database-heavy query.
17 *
18 * JS-owned tools (plugin slash-commands) take a different path — the
19 * shell harvests the command registry client-side, passes metadata
20 * to the AI as tools, receives a `tool_call` answer_type back, and
21 * invokes the command's `run()` locally. See `src/ai/ask.ts`.
22 *
23 * Example:
24 *
25 * ```php
26 * desktop_mode_register_ai_tool( array(
27 * 'name' => 'list_recent_orders',
28 * 'description' => 'List the site\'s most recent WooCommerce orders.',
29 * 'parameters' => array(
30 * 'type' => 'object',
31 * 'properties' => array(
32 * 'limit' => array( 'type' => 'integer' ),
33 * 'status' => array( 'type' => 'string', 'enum' => array( 'processing', 'completed' ) ),
34 * ),
35 * 'required' => array( 'limit' ),
36 * ),
37 * 'handler' => 'my_plugin_list_orders',
38 * 'capability' => 'manage_woocommerce',
39 * 'progress_message' => 'Checking recent orders…',
40 * ) );
41 * ```
42 *
43 * Handlers receive `( array $args, int $user_id )` and return
44 * `array|WP_Error`. An error return is caught, reported via the
45 * `desktop_mode_ai_search_error` action, and relayed to the model as a
46 * structured `{ error: ... }` tool result so the agent can decide
47 * whether to try another tool.
48 *
49 * @since 0.17.0
50 * @package WPDesktopMode
51 */
52
53 /**
54 * Register a server-side AI tool.
55 *
56 * @since 0.17.0
57 *
58 * @param array $args {
59 * @type string $name Unique tool name, `[a-z0-9_]+` (1-64 chars).
60 * Namespace with your plugin prefix — e.g. `woocommerce_list_orders`.
61 * Required.
62 * @type string $description One-line description shown to the model as part of the tool spec.
63 * Required.
64 * @type array $parameters JSON-Schema object describing the tool's arguments. Follow the
65 * OpenAI function-calling shape — `{ type: "object", properties: {...},
66 * required: [...] }`. Default `{ type: "object", properties: [] }`
67 * for tools that take no arguments.
68 * @type callable $handler `function( array $args, int $user_id ): array|WP_Error`. Return
69 * value is JSON-encoded and passed back to the model. Required.
70 * @type string $capability WordPress capability the current user must hold for the tool to
71 * be visible to the model. Checked BEFORE the tool is included in
72 * the request — unauthorised users never even see it exists.
73 * Default empty (visible to all logged-in users with AI enabled).
74 * @type string $progress_message Short phrase shown to the user while the tool runs (SSE progress
75 * events). Falls back to a generic "Working…" when empty.
76 * }
77 * @return true|WP_Error `true` on success, `WP_Error` on validation failure.
78 */
79 function desktop_mode_register_ai_tool( $args ) {
80 $defaults = array(
81 'name' => '',
82 'description' => '',
83 'parameters' => array(
84 'type' => 'object',
85 'properties' => (object) array(),
86 ),
87 'handler' => null,
88 'capability' => '',
89 'progress_message' => '',
90 );
91 $args = wp_parse_args( $args, $defaults );
92
93 $name = (string) $args['name'];
94 if ( '' === $name || ! preg_match( '/^[a-z0-9_]{1,64}$/', $name ) ) {
95 return desktop_mode_registration_error(
96 'desktop_mode_ai_tool_invalid_name',
97 __( 'AI tool registration requires a `name` matching [a-z0-9_]{1,64}.', 'desktop-mode' ),
98 array( 'name' => $name )
99 );
100 }
101 if ( '' === (string) $args['description'] ) {
102 return desktop_mode_registration_error(
103 'desktop_mode_ai_tool_missing_description',
104 __( 'AI tool registration requires a non-empty `description`.', 'desktop-mode' ),
105 array( 'name' => $name )
106 );
107 }
108 if ( ! is_callable( $args['handler'] ) ) {
109 return desktop_mode_registration_error(
110 'desktop_mode_ai_tool_invalid_handler',
111 __( 'AI tool registration requires a callable `handler`.', 'desktop-mode' ),
112 array( 'name' => $name )
113 );
114 }
115 if ( ! is_array( $args['parameters'] ) && ! is_object( $args['parameters'] ) ) {
116 return desktop_mode_registration_error(
117 'desktop_mode_ai_tool_invalid_parameters',
118 __( 'AI tool `parameters` must be a JSON-Schema object.', 'desktop-mode' ),
119 array( 'name' => $name )
120 );
121 }
122
123 $entry = array(
124 'name' => $name,
125 'description' => (string) $args['description'],
126 'parameters' => $args['parameters'],
127 'handler' => $args['handler'],
128 'capability' => (string) $args['capability'],
129 'progress_message' => (string) $args['progress_message'],
130 );
131 desktop_mode_desktop_ai_tool_registry( $name, $entry );
132
133 /**
134 * Fires after a desktop AI tool is successfully registered.
135 *
136 * @since 0.17.0
137 *
138 * @param string $name The tool name.
139 * @param array $entry The stored registry entry (handler included).
140 */
141 do_action( 'desktop_mode_ai_tool_registered', $name, $entry );
142
143 return true;
144 }
145
146 /**
147 * Internal module-level registry for AI tools declared via
148 * {@see desktop_mode_register_ai_tool()}.
149 *
150 * @since 0.17.0
151 * @internal
152 *
153 * @param string $name Tool name to read or write.
154 * @param array|null $entry Entry to store, or `null` to read.
155 * @return array|null|array<string,array>
156 */
157 function desktop_mode_desktop_ai_tool_registry( $name = '', $entry = null ) {
158 static $store = array();
159
160 if ( '' === (string) $name ) {
161 return $store;
162 }
163 if ( null !== $entry ) {
164 $store[ (string) $name ] = $entry;
165 }
166 return isset( $store[ (string) $name ] ) ? $store[ (string) $name ] : null;
167 }
168
169 /**
170 * Capability-filtered list of registered AI tools for the current user.
171 *
172 * Walks the registry, drops any tool whose `capability` the current
173 * user doesn't hold, and runs the surviving list through the
174 * `desktop_mode_ai_tools` filter alongside the built-in tools.
175 *
176 * @since 0.17.0
177 *
178 * @param int $user_id User whose capabilities gate visibility.
179 * @return array[] List of tool entries with `handler` still attached —
180 * the caller is expected to strip `handler` before
181 * sending the definitions to OpenAI.
182 */
183 function desktop_mode_get_registered_ai_tools_for_user( $user_id ) {
184 $registry = desktop_mode_desktop_ai_tool_registry();
185 if ( ! is_array( $registry ) || empty( $registry ) ) {
186 return array();
187 }
188
189 $user = $user_id > 0 ? get_user_by( 'id', $user_id ) : null;
190 $out = array();
191 foreach ( $registry as $entry ) {
192 $cap = (string) ( $entry['capability'] ?? '' );
193 if ( '' !== $cap && ( ! $user || ! user_can( $user, $cap ) ) ) {
194 continue;
195 }
196 $out[] = $entry;
197 }
198 return $out;
199 }
200
201 /**
202 * Project a registered tool entry into the OpenAI function-calling
203 * tool definition shape. Strips the handler + internal metadata.
204 *
205 * @since 0.17.0
206 *
207 * @param array $entry Registry entry.
208 * @return array OpenAI tool definition.
209 */
210 function desktop_mode_ai_tool_entry_to_definition( array $entry ) {
211 return array(
212 'type' => 'function',
213 'name' => (string) $entry['name'],
214 'description' => (string) $entry['description'],
215 'parameters' => $entry['parameters'],
216 );
217 }
218
219 /**
220 * Invoke a registered tool's handler, translating exceptions and
221 * `WP_Error` returns into a structured error payload the model can
222 * reason about without aborting the run.
223 *
224 * @since 0.17.0
225 *
226 * @param array $entry Registry entry (must include `handler`).
227 * @param array $args Decoded arguments from the model's tool call.
228 * @param int $user_id Current user (forwarded to the handler).
229 * @return array JSON-serialisable payload (error envelope on failure).
230 */
231 function desktop_mode_ai_invoke_registered_tool( array $entry, array $args, $user_id ) {
232 $name = (string) ( $entry['name'] ?? '' );
233 try {
234 $result = call_user_func( $entry['handler'], $args, (int) $user_id );
235 } catch ( \Throwable $e ) {
236 /**
237 * Fires when an AI tool handler throws.
238 *
239 * @since 0.17.0
240 *
241 * @param array $error { code, message, data } scrubbed of
242 * exception internals.
243 * @param string $name Tool name that threw.
244 * @param \Throwable $e The caught exception.
245 */
246 do_action(
247 'desktop_mode_ai_search_error',
248 array(
249 'code' => 'desktop_mode_ai_tool_exception',
250 'message' => 'Tool handler threw.',
251 'data' => array( 'tool' => $name ),
252 ),
253 $name,
254 $e
255 );
256 return array(
257 'error' => 'tool_exception',
258 'tool' => $name,
259 'message' => 'The tool failed. Try a different approach.',
260 );
261 }
262
263 if ( is_wp_error( $result ) ) {
264 return array(
265 'error' => $result->get_error_code(),
266 'tool' => $name,
267 'message' => $result->get_error_message(),
268 );
269 }
270 if ( ! is_array( $result ) ) {
271 return array(
272 'error' => 'tool_bad_return',
273 'tool' => $name,
274 'message' => 'Handler did not return an array.',
275 );
276 }
277 return $result;
278 }
279