PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.13.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.13.3
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 / OpenAIProvider.php

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

244 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenAIProvider — GPT via the OpenAI Chat Completions API.
4 *
5 * @package WPFunnels\AI
6 * @since 3.13.0
7 */
8
9 namespace WPFunnels\AI\Providers;
10
11 defined( 'ABSPATH' ) || exit;
12
13 /**
14 * Class OpenAIProvider
15 */
16 class OpenAIProvider extends AbstractProvider {
17
18 /**
19 * API base.
20 */
21 private const API_BASE = 'https://api.openai.com/v1';
22
23 /**
24 * Provider slug.
25 *
26 * @return string
27 */
28 public function slug() {
29 return 'openai';
30 }
31
32 /**
33 * Run one model turn.
34 *
35 * @param string $system System prompt.
36 * @param array $messages Normalized message list.
37 * @param array $tools Normalized tool definitions.
38 * @param bool $force_tool_use Force a tool call instead of a text-only reply.
39 * @return array|\WP_Error
40 */
41 public function chat( $system, $messages, $tools, $force_tool_use = false ) {
42 $body = [
43 'model' => $this->model,
44 'max_completion_tokens' => self::MAX_TOKENS,
45 'messages' => array_merge(
46 [
47 [
48 'role' => 'system',
49 'content' => $system,
50 ],
51 ],
52 $this->buildMessages( $messages )
53 ),
54 ];
55
56 if ( ! empty( $tools ) ) {
57 $body['tools'] = array_map(
58 static function ( $tool ) {
59 return [
60 'type' => 'function',
61 'function' => [
62 'name' => self::wireName( $tool['name'] ),
63 'description' => $tool['description'],
64 'parameters' => $tool['input_schema'],
65 ],
66 ];
67 },
68 $tools
69 );
70 // 'required' on the turn's first call — model must act, not narrate
71 // ("I'll do X now...") with no tool_calls. See ProviderInterface::chat().
72 $body['tool_choice'] = $force_tool_use ? 'required' : 'auto';
73 }
74
75 $response = $this->postJson(
76 self::API_BASE . '/chat/completions',
77 [ 'Authorization' => 'Bearer ' . $this->api_key ],
78 $body
79 );
80 if ( is_wp_error( $response ) ) {
81 return $response;
82 }
83
84 $choice = isset( $response['choices'][0] ) ? $response['choices'][0] : [];
85 $message = isset( $choice['message'] ) ? $choice['message'] : [];
86
87 $tool_calls = [];
88 $calls = isset( $message['tool_calls'] ) ? (array) $message['tool_calls'] : [];
89 foreach ( $calls as $call ) {
90 $raw_arguments = isset( $call['function']['arguments'] ) ? (string) $call['function']['arguments'] : '{}';
91 $arguments = json_decode( $raw_arguments, true );
92
93 $tool_calls[] = [
94 'id' => isset( $call['id'] ) ? $call['id'] : uniqid( 'call_' ),
95 'name' => self::abilityName( isset( $call['function']['name'] ) ? (string) $call['function']['name'] : '' ),
96 'arguments' => is_array( $arguments ) ? $arguments : [],
97 ];
98 }
99
100 $finish = isset( $choice['finish_reason'] ) ? $choice['finish_reason'] : 'stop';
101 $stop_map = [
102 'tool_calls' => 'tool_use',
103 'length' => 'max_tokens',
104 ];
105
106 return [
107 'text' => isset( $message['content'] ) ? (string) $message['content'] : '',
108 'tool_calls' => $tool_calls,
109 'stop_reason' => isset( $stop_map[ $finish ] ) ? $stop_map[ $finish ] : 'end_turn',
110 'raw' => $message,
111 'usage' => [
112 'input_tokens' => isset( $response['usage']['prompt_tokens'] ) ? (int) $response['usage']['prompt_tokens'] : 0,
113 'output_tokens' => isset( $response['usage']['completion_tokens'] ) ? (int) $response['usage']['completion_tokens'] : 0,
114 ],
115 ];
116 }
117
118 /**
119 * Cheap live credential check.
120 *
121 * @return true|\WP_Error
122 */
123 public function validateKey() {
124 $result = $this->getJson(
125 self::API_BASE . '/models',
126 [ 'Authorization' => 'Bearer ' . $this->api_key ]
127 );
128 return is_wp_error( $result ) ? $result : true;
129 }
130
131 /**
132 * Generate an image via the Images API, base64-encoded so the caller can
133 * write it straight to disk with no second (URL-fetch) network hop.
134 *
135 * @param string $prompt Image description.
136 * @param string $size One of '1024x1024', '1792x1024', '1024x1792'.
137 * @return array|\WP_Error Response with a 'data' array of {b64_json}.
138 */
139 public function generateImage( $prompt, $size = '1024x1024' ) {
140 return $this->postJson(
141 self::API_BASE . '/images/generations',
142 [ 'Authorization' => 'Bearer ' . $this->api_key ],
143 [
144 'model' => 'dall-e-3',
145 'prompt' => $prompt,
146 'n' => 1,
147 'size' => $size,
148 'response_format' => 'b64_json',
149 ]
150 );
151 }
152
153 // -------------------------------------------------------------------------
154 // History conversion
155 // -------------------------------------------------------------------------
156
157 /**
158 * Convert normalized history into Chat Completions messages.
159 *
160 * @param array $messages Normalized message list.
161 * @return array
162 */
163 private function buildMessages( $messages ) {
164 $out = [];
165
166 foreach ( $messages as $message ) {
167 $role = isset( $message['role'] ) ? $message['role'] : '';
168 $content = self::content( $message );
169
170 if ( 'user' === $role ) {
171 $out[] = [
172 'role' => 'user',
173 'content' => isset( $content['text'] ) ? (string) $content['text'] : '',
174 ];
175 continue;
176 }
177
178 if ( 'assistant' === $role ) {
179 $raw = $this->rawFor( $message );
180 if ( is_array( $raw ) && ! empty( $raw ) ) {
181 $out[] = array_merge( [ 'role' => 'assistant' ], $raw );
182 continue;
183 }
184
185 $assistant = [
186 'role' => 'assistant',
187 'content' => isset( $content['text'] ) ? (string) $content['text'] : '',
188 ];
189
190 $calls = isset( $content['tool_calls'] ) ? (array) $content['tool_calls'] : [];
191 $mapped = [];
192 foreach ( $calls as $call ) {
193 $arguments = isset( $call['arguments'] ) && is_array( $call['arguments'] ) ? $call['arguments'] : [];
194 $mapped[] = [
195 'id' => isset( $call['id'] ) ? $call['id'] : uniqid( 'call_' ),
196 'type' => 'function',
197 'function' => [
198 'name' => self::wireName( isset( $call['name'] ) ? $call['name'] : '' ),
199 'arguments' => wp_json_encode( $arguments ),
200 ],
201 ];
202 }
203 if ( ! empty( $mapped ) ) {
204 $assistant['tool_calls'] = $mapped;
205 }
206
207 $out[] = $assistant;
208 continue;
209 }
210
211 if ( 'tool' === $role ) {
212 $out[] = [
213 'role' => 'tool',
214 'tool_call_id' => isset( $content['tool_call_id'] ) ? (string) $content['tool_call_id'] : '',
215 'content' => isset( $content['content'] ) ? (string) $content['content'] : '',
216 ];
217 }
218 }
219
220 return $out;
221 }
222
223 /**
224 * OpenAI function names must match ^[a-zA-Z0-9_-]+$ — slashes in ability
225 * names ('wpfunnels/list-funnels') are not allowed.
226 *
227 * @param string $ability_name Ability name.
228 * @return string
229 */
230 private static function wireName( $ability_name ) {
231 return str_replace( '/', '__', (string) $ability_name );
232 }
233
234 /**
235 * Reverse of wireName().
236 *
237 * @param string $wire_name Wire name.
238 * @return string
239 */
240 private static function abilityName( $wire_name ) {
241 return str_replace( '__', '/', (string) $wire_name );
242 }
243 }
244