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

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

235 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GeminiProvider — Gemini via the Google Generative Language 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 GeminiProvider
15 */
16 class GeminiProvider extends AbstractProvider {
17
18 /**
19 * API base.
20 */
21 private const API_BASE = 'https://generativelanguage.googleapis.com/v1beta';
22
23 /**
24 * Provider slug.
25 *
26 * @return string
27 */
28 public function slug() {
29 return 'gemini';
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 function 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 'systemInstruction' => [ 'parts' => [ [ 'text' => $system ] ] ],
44 'contents' => $this->buildContents( $messages ),
45 'generationConfig' => [ 'maxOutputTokens' => self::MAX_TOKENS ],
46 ];
47
48 if ( ! empty( $tools ) ) {
49 $body['tools'] = [
50 [
51 'functionDeclarations' => array_map(
52 static function ( $tool ) {
53 return [
54 'name' => self::wireName( $tool['name'] ),
55 'description' => $tool['description'],
56 'parameters' => SchemaTranslator::forGemini( $tool['input_schema'] ),
57 ];
58 },
59 $tools
60 ),
61 ],
62 ];
63
64 // 'ANY' on the turn's first call — model must act, not narrate
65 // ("I'll do X now...") with no functionCall part.
66 $body['toolConfig'] = [
67 'functionCallingConfig' => [ 'mode' => $force_tool_use ? 'ANY' : 'AUTO' ],
68 ];
69 }
70
71 $response = $this->postJson(
72 self::API_BASE . '/models/' . rawurlencode( $this->model ) . ':generateContent',
73 [ 'x-goog-api-key' => $this->api_key ],
74 $body
75 );
76 if ( is_wp_error( $response ) ) {
77 return $response;
78 }
79
80 $candidate = isset( $response['candidates'][0] ) ? $response['candidates'][0] : [];
81 $parts = isset( $candidate['content']['parts'] ) ? (array) $candidate['content']['parts'] : [];
82
83 $text = '';
84 $tool_calls = [];
85 foreach ( $parts as $part ) {
86 if ( isset( $part['text'] ) ) {
87 $text .= $part['text'];
88 }
89 if ( isset( $part['functionCall'] ) ) {
90 // Gemini has no call ids — mint stable ones for the loop.
91 $tool_calls[] = [
92 'id' => uniqid( 'gcall_' ),
93 'name' => self::abilityName( isset( $part['functionCall']['name'] ) ? (string) $part['functionCall']['name'] : '' ),
94 'arguments' => isset( $part['functionCall']['args'] ) && is_array( $part['functionCall']['args'] ) ? $part['functionCall']['args'] : [],
95 ];
96 }
97 }
98
99 $finish = isset( $candidate['finishReason'] ) ? (string) $candidate['finishReason'] : 'STOP';
100 $stop_reason = 'end_turn';
101 if ( ! empty( $tool_calls ) ) {
102 $stop_reason = 'tool_use';
103 } elseif ( 'MAX_TOKENS' === $finish ) {
104 $stop_reason = 'max_tokens';
105 }
106
107 return [
108 'text' => $text,
109 'tool_calls' => $tool_calls,
110 'stop_reason' => $stop_reason,
111 'raw' => isset( $candidate['content'] ) ? $candidate['content'] : [],
112 'usage' => [
113 'input_tokens' => isset( $response['usageMetadata']['promptTokenCount'] ) ? (int) $response['usageMetadata']['promptTokenCount'] : 0,
114 'output_tokens' => isset( $response['usageMetadata']['candidatesTokenCount'] ) ? (int) $response['usageMetadata']['candidatesTokenCount'] : 0,
115 ],
116 ];
117 }
118
119 /**
120 * Cheap live credential check.
121 *
122 * @return true|\WP_Error
123 */
124 public function validateKey() {
125 $result = $this->getJson(
126 self::API_BASE . '/models?pageSize=1',
127 [ 'x-goog-api-key' => $this->api_key ]
128 );
129 return is_wp_error( $result ) ? $result : true;
130 }
131
132 // -------------------------------------------------------------------------
133 // History conversion
134 // -------------------------------------------------------------------------
135
136 /**
137 * Convert normalized history into Gemini contents.
138 *
139 * @param array $messages Normalized message list.
140 * @return array
141 */
142 private function buildContents( $messages ) {
143 $out = [];
144
145 foreach ( $messages as $message ) {
146 $role = isset( $message['role'] ) ? $message['role'] : '';
147 $content = self::content( $message );
148
149 if ( 'user' === $role ) {
150 $out[] = [
151 'role' => 'user',
152 'parts' => [ [ 'text' => isset( $content['text'] ) ? (string) $content['text'] : '' ] ],
153 ];
154 continue;
155 }
156
157 if ( 'assistant' === $role ) {
158 $raw = $this->rawFor( $message );
159 if ( is_array( $raw ) && ! empty( $raw['parts'] ) ) {
160 $out[] = [
161 'role' => 'model',
162 'parts' => $raw['parts'],
163 ];
164 continue;
165 }
166
167 $parts = [];
168 if ( isset( $content['text'] ) && '' !== (string) $content['text'] ) {
169 $parts[] = [ 'text' => (string) $content['text'] ];
170 }
171 $calls = isset( $content['tool_calls'] ) ? (array) $content['tool_calls'] : [];
172 foreach ( $calls as $call ) {
173 $parts[] = [
174 'functionCall' => [
175 'name' => self::wireName( isset( $call['name'] ) ? $call['name'] : '' ),
176 'args' => isset( $call['arguments'] ) && is_array( $call['arguments'] ) ? $call['arguments'] : new \stdClass(),
177 ],
178 ];
179 }
180 if ( ! empty( $parts ) ) {
181 $out[] = [
182 'role' => 'model',
183 'parts' => $parts,
184 ];
185 }
186 continue;
187 }
188
189 if ( 'tool' === $role ) {
190 $raw_content = isset( $content['content'] ) ? (string) $content['content'] : '';
191 $decoded = json_decode( $raw_content, true );
192 $part = [
193 'functionResponse' => [
194 'name' => self::wireName( isset( $content['name'] ) ? (string) $content['name'] : '' ),
195 'response' => [ 'result' => null !== $decoded ? $decoded : $raw_content ],
196 ],
197 ];
198
199 // Consecutive function responses merge into one user turn.
200 $last = count( $out ) - 1;
201 if ( $last >= 0 && 'user' === $out[ $last ]['role'] && isset( $out[ $last ]['parts'][0]['functionResponse'] ) ) {
202 $out[ $last ]['parts'][] = $part;
203 } else {
204 $out[] = [
205 'role' => 'user',
206 'parts' => [ $part ],
207 ];
208 }
209 }
210 }
211
212 return $out;
213 }
214
215 /**
216 * Gemini function names must match ^[a-zA-Z_][a-zA-Z0-9_.-]* — no slashes.
217 *
218 * @param string $ability_name Ability name.
219 * @return string
220 */
221 private static function wireName( $ability_name ) {
222 return str_replace( '/', '__', (string) $ability_name );
223 }
224
225 /**
226 * Reverse of wireName().
227 *
228 * @param string $wire_name Wire name.
229 * @return string
230 */
231 private static function abilityName( $wire_name ) {
232 return str_replace( '__', '/', (string) $wire_name );
233 }
234 }
235