PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.3
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.3, at includes/ai-copilot/tools-registry.php

283 lines 10.0 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`. A thrown exception is caught, reported via the
45 * `desktop_mode_ai_search_error` action, and relayed to the model as a
46 * structured `{ error: ... }` tool result; a `WP_Error` return is
47 * relayed to the model the same way (without firing the action) so the
48 * agent can decide whether to try another tool.
49 *
50 * @since 0.5.1
51 * @package WPDesktopMode
52 */
53
54 /**
55 * Register a server-side AI tool.
56 *
57 * @since 0.5.1
58 *
59 * @param array $args {
60 * @type string $name Unique tool name, `[a-z0-9_]+` (1-64 chars).
61 * Namespace with your plugin prefix — e.g. `woocommerce_list_orders`.
62 * Required.
63 * @type string $description One-line description shown to the model as part of the tool spec.
64 * Required.
65 * @type array $parameters JSON-Schema object describing the tool's arguments. Follow the
66 * OpenAI function-calling shape — `{ type: "object", properties: {...},
67 * required: [...] }`. Default `{ type: "object", properties: {} }`
68 * for tools that take no arguments (in PHP, use `(object) array()` or
69 * `new stdClass()` for `properties` so it JSON-encodes as an object,
70 * not an array).
71 * @type callable $handler `function( array $args, int $user_id ): array|WP_Error`. Return
72 * value is JSON-encoded and passed back to the model. Required.
73 * @type string $capability WordPress capability the current user must hold for the tool to
74 * be visible to the model. Checked BEFORE the tool is included in
75 * the request — unauthorised users never even see it exists.
76 * Default empty (visible to all logged-in users with AI enabled).
77 * @type string $progress_message Short phrase shown to the user while the tool runs (SSE progress
78 * events). Falls back to a generic "Working…" when empty.
79 * }
80 * @return true|WP_Error `true` on success, `WP_Error` on validation failure.
81 */
82 function desktop_mode_register_ai_tool( $args ) {
83 $defaults = array(
84 'name' => '',
85 'description' => '',
86 'parameters' => array(
87 'type' => 'object',
88 'properties' => (object) array(),
89 ),
90 'handler' => null,
91 'capability' => '',
92 'progress_message' => '',
93 );
94 $args = wp_parse_args( $args, $defaults );
95
96 $name = (string) $args['name'];
97 if ( '' === $name || ! preg_match( '/^[a-z0-9_]{1,64}$/', $name ) ) {
98 return desktop_mode_registration_error(
99 'desktop_mode_ai_tool_invalid_name',
100 __( 'AI tool registration requires a `name` matching [a-z0-9_]{1,64}.', 'desktop-mode' ),
101 array( 'name' => $name )
102 );
103 }
104 if ( '' === (string) $args['description'] ) {
105 return desktop_mode_registration_error(
106 'desktop_mode_ai_tool_missing_description',
107 __( 'AI tool registration requires a non-empty `description`.', 'desktop-mode' ),
108 array( 'name' => $name )
109 );
110 }
111 if ( ! is_callable( $args['handler'] ) ) {
112 return desktop_mode_registration_error(
113 'desktop_mode_ai_tool_invalid_handler',
114 __( 'AI tool registration requires a callable `handler`.', 'desktop-mode' ),
115 array( 'name' => $name )
116 );
117 }
118 if ( ! is_array( $args['parameters'] ) && ! is_object( $args['parameters'] ) ) {
119 return desktop_mode_registration_error(
120 'desktop_mode_ai_tool_invalid_parameters',
121 __( 'AI tool `parameters` must be a JSON-Schema object.', 'desktop-mode' ),
122 array( 'name' => $name )
123 );
124 }
125
126 $entry = array(
127 'name' => $name,
128 'description' => (string) $args['description'],
129 'parameters' => $args['parameters'],
130 'handler' => $args['handler'],
131 'capability' => (string) $args['capability'],
132 'progress_message' => (string) $args['progress_message'],
133 );
134 desktop_mode_desktop_ai_tool_registry( $name, $entry );
135
136 /**
137 * Fires after a desktop AI tool is successfully registered.
138 *
139 * @since 0.5.1
140 *
141 * @param string $name The tool name.
142 * @param array $entry The stored registry entry (handler included).
143 */
144 do_action( 'desktop_mode_ai_tool_registered', $name, $entry );
145
146 return true;
147 }
148
149 /**
150 * Internal module-level registry for AI tools declared via
151 * {@see desktop_mode_register_ai_tool()}.
152 *
153 * @since 0.5.1
154 * @internal
155 *
156 * @param string $name Tool name to read or write.
157 * @param array|null $entry Entry to store, or `null` to read.
158 * @return array|null|array<string,array>
159 */
160 function desktop_mode_desktop_ai_tool_registry( $name = '', $entry = null ) {
161 static $store = array();
162
163 if ( '' === (string) $name ) {
164 return $store;
165 }
166 if ( null !== $entry ) {
167 $store[ (string) $name ] = $entry;
168 }
169 return isset( $store[ (string) $name ] ) ? $store[ (string) $name ] : null;
170 }
171
172 /**
173 * Capability-filtered list of registered AI tools for the current user.
174 *
175 * Walks the registry and drops any tool whose `capability` the current
176 * user doesn't hold. The caller (the agent loop in search.php) later
177 * merges the result with the built-in and command tools and runs the
178 * combined list through the `desktop_mode_ai_tools` filter.
179 *
180 * @since 0.5.1
181 *
182 * @param int $user_id User whose capabilities gate visibility.
183 * @return array[] List of tool entries with `handler` still attached —
184 * the caller is expected to strip `handler` before
185 * sending the definitions to OpenAI.
186 */
187 function desktop_mode_get_registered_ai_tools_for_user( $user_id ) {
188 $registry = desktop_mode_desktop_ai_tool_registry();
189 if ( ! is_array( $registry ) || empty( $registry ) ) {
190 return array();
191 }
192
193 $user = $user_id > 0 ? get_user_by( 'id', $user_id ) : null;
194 $out = array();
195 foreach ( $registry as $entry ) {
196 $cap = (string) ( $entry['capability'] ?? '' );
197 if ( '' !== $cap && ( ! $user || ! user_can( $user, $cap ) ) ) {
198 continue;
199 }
200 $out[] = $entry;
201 }
202 return $out;
203 }
204
205 /**
206 * Project a registered tool entry into the OpenAI function-calling
207 * tool definition shape. Strips the handler + internal metadata.
208 *
209 * @since 0.5.1
210 *
211 * @param array $entry Registry entry.
212 * @return array OpenAI tool definition.
213 */
214 function desktop_mode_ai_tool_entry_to_definition( array $entry ) {
215 return array(
216 'type' => 'function',
217 'name' => (string) $entry['name'],
218 'description' => (string) $entry['description'],
219 'parameters' => $entry['parameters'],
220 );
221 }
222
223 /**
224 * Invoke a registered tool's handler, translating exceptions and
225 * `WP_Error` returns into a structured error payload the model can
226 * reason about without aborting the run.
227 *
228 * @since 0.5.1
229 *
230 * @param array $entry Registry entry (must include `handler`).
231 * @param array $args Decoded arguments from the model's tool call.
232 * @param int $user_id Current user (forwarded to the handler).
233 * @return array JSON-serialisable payload (error envelope on failure).
234 */
235 function desktop_mode_ai_invoke_registered_tool( array $entry, array $args, $user_id ) {
236 $name = (string) ( $entry['name'] ?? '' );
237 try {
238 $result = call_user_func( $entry['handler'], $args, (int) $user_id );
239 } catch ( \Throwable $e ) {
240 /**
241 * Fires when an AI tool handler throws.
242 *
243 * @since 0.5.1
244 *
245 * @param array $error { code, message, data } scrubbed of
246 * exception internals.
247 * @param string $name Tool name that threw.
248 * @param \Throwable $e The caught exception.
249 */
250 do_action(
251 'desktop_mode_ai_search_error',
252 array(
253 'code' => 'desktop_mode_ai_tool_exception',
254 'message' => 'Tool handler threw.',
255 'data' => array( 'tool' => $name ),
256 ),
257 $name,
258 $e
259 );
260 return array(
261 'error' => 'tool_exception',
262 'tool' => $name,
263 'message' => 'The tool failed. Try a different approach.',
264 );
265 }
266
267 if ( is_wp_error( $result ) ) {
268 return array(
269 'error' => $result->get_error_code(),
270 'tool' => $name,
271 'message' => $result->get_error_message(),
272 );
273 }
274 if ( ! is_array( $result ) ) {
275 return array(
276 'error' => 'tool_bad_return',
277 'tool' => $name,
278 'message' => 'Handler did not return an array.',
279 );
280 }
281 return $result;
282 }
283