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 / AnthropicProvider.php

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

274 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AnthropicProvider — Claude via the Anthropic Messages 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 AnthropicProvider
15 */
16 class AnthropicProvider extends AbstractProvider {
17
18 /**
19 * Messages endpoint.
20 */
21 private const API_URL = 'https://api.anthropic.com/v1/messages';
22
23 /**
24 * Pinned API version.
25 */
26 private const API_VERSION = '2023-06-01';
27
28 /**
29 * Provider slug.
30 *
31 * @return string
32 */
33 public function slug() {
34 return 'anthropic';
35 }
36
37 /**
38 * Run one model turn.
39 *
40 * @param string $system System prompt.
41 * @param array $messages Normalized message list.
42 * @param array $tools Normalized tool definitions.
43 * @param bool $force_tool_use Force a tool call instead of a text-only reply.
44 * @return array|\WP_Error
45 */
46 public function chat( $system, $messages, $tools, $force_tool_use = false ) {
47 $body = [
48 'model' => $this->model,
49 'max_tokens' => self::MAX_TOKENS,
50 // Stable system prompt first with a cache breakpoint — history grows
51 // after it, so repeated loop steps read the prefix from cache.
52 'system' => [
53 [
54 'type' => 'text',
55 'text' => $system,
56 'cache_control' => [ 'type' => 'ephemeral' ],
57 ],
58 ],
59 'messages' => $this->buildMessages( $messages ),
60 ];
61
62 // Extended thinking only supports tool_choice 'auto' (or none) — the API
63 // rejects a forced choice alongside it. On the turn's first call we need
64 // the forced call (model must act, not narrate with no tool_calls) more
65 // than the extra reasoning, so thinking is skipped for that one request.
66 if ( $force_tool_use && ! empty( $tools ) ) {
67 $body['tool_choice'] = [ 'type' => 'any' ];
68 } else {
69 $body['thinking'] = [ 'type' => 'adaptive' ];
70 }
71
72 if ( ! empty( $tools ) ) {
73 $body['tools'] = array_map(
74 static function ( $tool ) {
75 return [
76 // Tool names must match ^[a-zA-Z0-9_-]{1,64}$ — no slashes.
77 'name' => self::wireName( $tool['name'] ),
78 'description' => $tool['description'],
79 'input_schema' => $tool['input_schema'],
80 ];
81 },
82 $tools
83 );
84 }
85
86 $response = $this->postJson(
87 self::API_URL,
88 [
89 'x-api-key' => $this->api_key,
90 'anthropic-version' => self::API_VERSION,
91 ],
92 $body
93 );
94 if ( is_wp_error( $response ) ) {
95 return $response;
96 }
97
98 $text = '';
99 $tool_calls = [];
100 $blocks = isset( $response['content'] ) ? (array) $response['content'] : [];
101
102 foreach ( $blocks as $block ) {
103 $type = isset( $block['type'] ) ? $block['type'] : '';
104
105 if ( 'text' === $type ) {
106 $text .= isset( $block['text'] ) ? $block['text'] : '';
107 } elseif ( 'tool_use' === $type ) {
108 $tool_calls[] = [
109 'id' => isset( $block['id'] ) ? $block['id'] : uniqid( 'toolu_' ),
110 'name' => self::abilityName( isset( $block['name'] ) ? (string) $block['name'] : '' ),
111 'arguments' => isset( $block['input'] ) && is_array( $block['input'] ) ? $block['input'] : [],
112 ];
113 }
114 }
115
116 $stop_map = [
117 'tool_use' => 'tool_use',
118 'max_tokens' => 'max_tokens',
119 ];
120 $stop_reason = isset( $response['stop_reason'] ) ? $response['stop_reason'] : '';
121
122 return [
123 'text' => $text,
124 'tool_calls' => $tool_calls,
125 'stop_reason' => isset( $stop_map[ $stop_reason ] ) ? $stop_map[ $stop_reason ] : 'end_turn',
126 // Full native content (incl. thinking blocks) — must be replayed
127 // verbatim on the next request of a tool-use loop.
128 'raw' => isset( $response['content'] ) ? $response['content'] : [],
129 'usage' => [
130 'input_tokens' => isset( $response['usage']['input_tokens'] ) ? (int) $response['usage']['input_tokens'] : 0,
131 'output_tokens' => isset( $response['usage']['output_tokens'] ) ? (int) $response['usage']['output_tokens'] : 0,
132 ],
133 ];
134 }
135
136 /**
137 * Cheap live credential check.
138 *
139 * @return true|\WP_Error
140 */
141 public function validateKey() {
142 $result = $this->postJson(
143 self::API_URL,
144 [
145 'x-api-key' => $this->api_key,
146 'anthropic-version' => self::API_VERSION,
147 ],
148 [
149 'model' => $this->model,
150 'max_tokens' => 1,
151 'messages' => [
152 [
153 'role' => 'user',
154 'content' => 'ping',
155 ],
156 ],
157 ]
158 );
159 return is_wp_error( $result ) ? $result : true;
160 }
161
162 // -------------------------------------------------------------------------
163 // History conversion
164 // -------------------------------------------------------------------------
165
166 /**
167 * Convert normalized history into Anthropic messages.
168 *
169 * @param array $messages Normalized message list.
170 * @return array
171 */
172 private function buildMessages( $messages ) {
173 $out = [];
174
175 foreach ( $messages as $message ) {
176 $role = isset( $message['role'] ) ? $message['role'] : '';
177 $content = self::content( $message );
178
179 if ( 'user' === $role ) {
180 $out[] = [
181 'role' => 'user',
182 'content' => isset( $content['text'] ) ? (string) $content['text'] : '',
183 ];
184 continue;
185 }
186
187 if ( 'assistant' === $role ) {
188 $raw = $this->rawFor( $message );
189 if ( is_array( $raw ) && ! empty( $raw ) ) {
190 $out[] = [
191 'role' => 'assistant',
192 'content' => $raw,
193 ];
194 continue;
195 }
196
197 // Reconstructed fallback (history produced by another provider).
198 $blocks = [];
199 if ( isset( $content['text'] ) && '' !== (string) $content['text'] ) {
200 $blocks[] = [
201 'type' => 'text',
202 'text' => (string) $content['text'],
203 ];
204 }
205 $calls = isset( $content['tool_calls'] ) ? (array) $content['tool_calls'] : [];
206 foreach ( $calls as $call ) {
207 $blocks[] = [
208 'type' => 'tool_use',
209 'id' => isset( $call['id'] ) ? $call['id'] : uniqid( 'toolu_' ),
210 'name' => self::wireName( isset( $call['name'] ) ? $call['name'] : '' ),
211 'input' => isset( $call['arguments'] ) && is_array( $call['arguments'] ) ? $call['arguments'] : new \stdClass(),
212 ];
213 }
214 if ( ! empty( $blocks ) ) {
215 $out[] = [
216 'role' => 'assistant',
217 'content' => $blocks,
218 ];
219 }
220 continue;
221 }
222
223 if ( 'tool' === $role ) {
224 $result_block = [
225 'type' => 'tool_result',
226 'tool_use_id' => isset( $content['tool_call_id'] ) ? (string) $content['tool_call_id'] : '',
227 'content' => isset( $content['content'] ) ? (string) $content['content'] : '',
228 ];
229 if ( ! empty( $content['is_error'] ) ) {
230 $result_block['is_error'] = true;
231 }
232
233 // Consecutive tool results merge into one user turn.
234 $last = count( $out ) - 1;
235 if ( $last >= 0
236 && 'user' === $out[ $last ]['role']
237 && is_array( $out[ $last ]['content'] )
238 && isset( $out[ $last ]['content'][0]['type'] )
239 && 'tool_result' === $out[ $last ]['content'][0]['type'] ) {
240 $out[ $last ]['content'][] = $result_block;
241 } else {
242 $out[] = [
243 'role' => 'user',
244 'content' => [ $result_block ],
245 ];
246 }
247 }
248 }
249
250 return $out;
251 }
252
253 /**
254 * Anthropic tool names must match ^[a-zA-Z0-9_-]{1,64}$ — slashes in ability
255 * names ('wpfunnels/list-funnels') are not allowed on the wire.
256 *
257 * @param string $ability_name Ability name.
258 * @return string
259 */
260 private static function wireName( $ability_name ) {
261 return str_replace( '/', '__', (string) $ability_name );
262 }
263
264 /**
265 * Reverse of wireName().
266 *
267 * @param string $wire_name Wire name.
268 * @return string
269 */
270 private static function abilityName( $wire_name ) {
271 return str_replace( '__', '/', (string) $wire_name );
272 }
273 }
274