PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.2
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.2
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / includes / core / AI / Providers / WordPressAIProvider.php

WordPressAIProvider.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.13.2, at includes/core/AI/Providers/WordPressAIProvider.php

270 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WordPressAIProvider — wraps the WordPress AI Services API.
4 *
5 * Delegates to whichever AI service the site owner configured via the WordPress
6 * AI Services plugin (wordpress.org/plugins/ai-services) or a hosting-level
7 * WordPress AI Client. No API key is stored in WPFunnels; the underlying
8 * service handles credentials — which also makes this the natural shared
9 * connection on sites running several plugins that need AI.
10 *
11 * Because AI Services does not guarantee tool-use support across all backends,
12 * this provider operates in text-generation mode: responses never carry
13 * tool_calls, so the copilot can answer from context but cannot execute write
14 * operations. For the full copilot (build funnels, configure offers), connect a
15 * direct key via Anthropic, OpenAI, or Gemini.
16 *
17 * @package WPFunnels\AI
18 * @since 3.13.0
19 */
20
21 namespace WPFunnels\AI\Providers;
22
23 defined( 'ABSPATH' ) || exit;
24
25 /**
26 * Class WordPressAIProvider
27 */
28 class WordPressAIProvider implements ProviderInterface {
29
30 /**
31 * Model id, or 'auto' to let the service decide.
32 *
33 * @var string
34 */
35 protected $model;
36
37 /**
38 * Constructor.
39 *
40 * @param string $api_key Ignored — the service owns credentials.
41 * @param string $model Model id or 'auto'.
42 */
43 public function __construct( $api_key = '', $model = '' ) {
44 unset( $api_key );
45 $this->model = '' !== (string) $model ? (string) $model : 'auto';
46 }
47
48 /**
49 * Provider slug.
50 *
51 * @return string
52 */
53 public function slug() {
54 return 'wordpress_ai';
55 }
56
57 /**
58 * Check whether a WordPress AI service is available and configured.
59 *
60 * @return true|\WP_Error
61 */
62 public function validateKey() {
63 if ( ! self::isAvailable() ) {
64 return new \WP_Error(
65 'wp_ai_unavailable',
66 __( 'WordPress AI is not available on this site. Install the AI Services plugin (wordpress.org/plugins/ai-services) or use hosting that provides the WordPress AI Client API.', 'wpfnl' )
67 );
68 }
69
70 if ( empty( self::getAvailableServices() ) ) {
71 return new \WP_Error(
72 'wp_ai_no_service',
73 __( 'WordPress AI is installed but no AI provider is configured under it. Add one under Settings → AI Services (or your hosting panel).', 'wpfnl' )
74 );
75 }
76
77 return true;
78 }
79
80 /**
81 * Run one text-generation turn via the WordPress AI Services API.
82 *
83 * Tool use is not attempted — tool_calls is always empty.
84 *
85 * @param string $system System prompt text.
86 * @param array $messages Normalized message list.
87 * @param array $tools Tool definitions (accepted but not forwarded).
88 * @param bool $force_tool_use Ignored — this provider never emits tool_calls.
89 * @return array|\WP_Error
90 */
91 public function chat( $system, $messages, $tools, $force_tool_use = false ) {
92 unset( $tools, $force_tool_use );
93
94 if ( ! self::isAvailable() ) {
95 return new \WP_Error( 'wp_ai_unavailable', __( 'WordPress AI is not available.', 'wpfnl' ) );
96 }
97
98 try {
99 return $this->doChat( $system, $messages );
100 } catch ( \Throwable $e ) {
101 return new \WP_Error( 'wp_ai_error', $e->getMessage() );
102 }
103 }
104
105 // -------------------------------------------------------------------------
106 // Internal helpers
107 // -------------------------------------------------------------------------
108
109 /**
110 * Perform the generation call.
111 *
112 * @param string $system System prompt text.
113 * @param array $messages Normalized message list.
114 * @return array|\WP_Error
115 * @throws \Throwable When the service throws.
116 */
117 private function doChat( $system, $messages ) {
118 $fn = self::servicesFunctionName();
119 $api = $fn();
120
121 $service = method_exists( $api, 'get_available_service' )
122 ? $api->get_available_service( [ 'feature' => 'text-generation' ] )
123 : null;
124
125 if ( null === $service || is_wp_error( $service ) ) {
126 return new \WP_Error( 'wp_ai_no_service', __( 'No WordPress AI service is available.', 'wpfnl' ) );
127 }
128
129 $model_args = [ 'feature' => 'text-generation' ];
130 if ( 'auto' !== $this->model && '' !== $this->model ) {
131 $model_args['model'] = $this->model;
132 }
133
134 $model_obj = method_exists( $service, 'get_model_object' )
135 ? $service->get_model_object( $model_args )
136 : null;
137
138 if ( null === $model_obj || is_wp_error( $model_obj ) ) {
139 return new \WP_Error( 'wp_ai_model_error', __( 'Could not load a WordPress AI model.', 'wpfnl' ) );
140 }
141
142 // Flatten messages into a prompt string with role prefixes.
143 $history_lines = [];
144 if ( '' !== (string) $system ) {
145 $history_lines[] = 'System: ' . $system;
146 }
147 foreach ( $messages as $message ) {
148 $role = isset( $message['role'] ) ? $message['role'] : 'user';
149 $content = isset( $message['content'] ) ? $message['content'] : [];
150
151 if ( is_string( $content ) ) {
152 $decoded = json_decode( $content, true );
153 $content = is_array( $decoded ) ? $decoded : [ 'text' => $content ];
154 }
155
156 if ( is_array( $content ) ) {
157 $text = isset( $content['text'] ) ? $content['text'] : wp_json_encode( $content );
158 } else {
159 $text = (string) $content;
160 }
161
162 $history_lines[] = ( 'assistant' === $role ? 'Assistant: ' : 'User: ' ) . $text;
163 }
164
165 $prompt = implode( "\n\n", $history_lines );
166 $candidates = method_exists( $model_obj, 'generate_text' )
167 ? $model_obj->generate_text( $prompt, [] )
168 : null;
169
170 if ( null === $candidates || is_wp_error( $candidates ) ) {
171 $message = is_wp_error( $candidates )
172 ? $candidates->get_error_message()
173 : __( 'WordPress AI returned no response.', 'wpfnl' );
174 return new \WP_Error( 'wp_ai_empty_response', $message );
175 }
176
177 return [
178 'text' => self::extractText( $candidates ),
179 'tool_calls' => [],
180 'stop_reason' => 'end_turn',
181 'raw' => null,
182 'usage' => [
183 'input_tokens' => 0,
184 'output_tokens' => 0,
185 ],
186 ];
187 }
188
189 /**
190 * Pull text out of whatever shape the installed AI Services version returns.
191 *
192 * @param mixed $candidates Generation result.
193 * @return string
194 */
195 private static function extractText( $candidates ) {
196 if ( function_exists( 'ai_candidates_text' ) ) {
197 return (string) ai_candidates_text( $candidates );
198 }
199 if ( is_string( $candidates ) ) {
200 return $candidates;
201 }
202
203 $text = '';
204 if ( is_object( $candidates ) && method_exists( $candidates, 'get_candidates' ) ) {
205 $list = $candidates->get_candidates();
206 $first = isset( $list[0] ) ? $list[0] : null;
207 if ( $first && method_exists( $first, 'get_content' ) ) {
208 foreach ( $first->get_content()->get_parts() as $part ) {
209 if ( method_exists( $part, 'get_text' ) ) {
210 $text .= $part->get_text();
211 }
212 }
213 }
214 }
215
216 return $text;
217 }
218
219 // -------------------------------------------------------------------------
220 // Static availability checks
221 // -------------------------------------------------------------------------
222
223 /**
224 * Whether a WordPress AI Services API is present.
225 *
226 * @return bool
227 */
228 public static function isAvailable() {
229 return '' !== self::servicesFunctionName();
230 }
231
232 /**
233 * Services configured under WordPress AI.
234 *
235 * @return array
236 */
237 public static function getAvailableServices() {
238 if ( ! self::isAvailable() ) {
239 return [];
240 }
241 try {
242 $fn = self::servicesFunctionName();
243 $api = $fn();
244 if ( method_exists( $api, 'get_available_services' ) ) {
245 $result = $api->get_available_services();
246 return is_array( $result ) ? $result : [];
247 }
248 } catch ( \Throwable $e ) {
249 // Swallow — treated as unavailable.
250 return [];
251 }
252 return [];
253 }
254
255 /**
256 * Name of the global accessor exposed by the installed AI Services version.
257 *
258 * @return string
259 */
260 private static function servicesFunctionName() {
261 if ( function_exists( 'ai_services' ) ) {
262 return 'ai_services';
263 }
264 if ( function_exists( 'wp_ai_services' ) ) {
265 return 'wp_ai_services';
266 }
267 return '';
268 }
269 }
270