PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.6.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.6.9
3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / google.php
ai-engine / classes / engines Last commit date
anthropic.php 1 year ago core.php 1 year ago factory.php 1 year ago google.php 1 year ago huggingface.php 1 year ago openai.php 1 year ago openrouter.php 1 year ago replicate.php 1 year ago
google.php
439 lines
1 <?php
2
3 class Meow_MWAI_Engines_Google extends Meow_MWAI_Engines_Core
4 {
5 // Base (Google)
6 protected $apiKey = null;
7 protected $region = null;
8 protected $projectId = null;
9 protected $endpoint = null;
10
11 // Response
12 protected $inModel = null;
13 protected $inId = null;
14
15 // Streaming
16 private $streamFunctionCall = null;
17
18 public function __construct( $core, $env )
19 {
20 parent::__construct( $core, $env );
21 $this->set_environment();
22 }
23
24 protected function set_environment() {
25 $env = $this->env;
26 $this->apiKey = $env['apikey'];
27 if ( $this->envType === 'google' ) {
28 // https://{REGION}-aiplatform.googleapis.com/v1/projects/{PROJECT_ID}/locations/{REGION}/publishers/google
29 $this->region = $env['region'];
30 $this->projectId = $env['projectId'];
31
32 // Google Cloud API
33 // $this->endpoint = apply_filters( 'mwai_google_endpoint', "https://{$this->region}-aiplatform.googleapis.com/v1/projects/{$this->projectId}/locations/{$this->region}/publishers/google", $this->env );
34
35 // Generative Language API (less issues with auth)
36 $this->endpoint = apply_filters( 'mwai_google_endpoint', "https://generativelanguage.googleapis.com/v1", $this->env );
37 }
38 else {
39 throw new Exception( 'Unknown environment type: ' . $this->envType );
40 }
41 }
42
43 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
44 function check_for_error( $data ) {
45 if ( strpos( $data, 'error' ) === false ) {
46 return;
47 }
48 if ( strpos( $data, 'data: ' ) === 0 ) {
49 $jsonPart = substr( $data, strlen( 'data: ' ) );
50 }
51 else {
52 $jsonPart = $data;
53 }
54 $json = json_decode( $jsonPart, true );
55 if ( json_last_error() === JSON_ERROR_NONE ) {
56 if ( isset( $json['error'] ) ) {
57 $error = $json['error'];
58 $code = $error['code'];
59 $message = $error['message'];
60 throw new Exception( "Error $code: $message" );
61 }
62 }
63 }
64
65 private function build_messages( $query ) {
66 $messages = [];
67
68 // First, we need to add the first message (the instructions).
69 if ( !empty( $query->instructions ) ) {
70 $messages[] = [ 'role' => 'model', 'parts' => [ [ 'text' => $query->instructions ] ] ];
71 }
72
73 // Then, if any, we need to add the 'messages', they are already formatted.
74 foreach ( $query->messages as $message ) {
75 // messages contains role and content (as OpenAI does it, but we need to convert it to Google's format)
76 // role's assistant should be model, and user should be user.
77 $newMessage = [ 'role' => $message['role'], 'parts' => [] ];
78 if ( isset( $message['content'] ) ) {
79 $newMessage['parts'][] = [ 'text' => $message['content'] ];
80 }
81 if ( $newMessage['role'] === 'assistant' ) {
82 $newMessage['role'] = 'model';
83 }
84 $messages[] = $newMessage;
85 }
86
87 // If there is a context, we need to add it.
88 if ( !empty( $query->context ) ) {
89 $messages[] = [ 'role' => 'model', 'parts' => [ [ 'text' => $query->context ] ] ];
90 }
91
92 // Finally, we need to add the message, but if there is an image, we need to add it as a model message.
93 if ( $query->attachedFile) {
94 // Gemini doesn't handle URL uploads, so we need to convert it to base64.
95 //$remote_upload = $this->core->get_option( 'image_remote_upload' );
96 $data = $query->attachedFile->get_base64();
97 //$data = strpos( $data, 'data:' ) === 0;
98 $messages[] = [
99 'role' => 'user',
100 'parts' => [
101 [
102 "inlineData" => [
103 "mimeType" => "image/jpeg",
104 "data" => $data // We need to be careful here to get only the data part
105 ]
106 ],
107 [
108 "text" => $query->get_message()
109 ]
110 ]
111 ];
112 // TODO: Gemini doesn't support multiturn chat with Vision...
113 // So we only keep the message that goes with the image.
114 $messages = array_slice( $messages, -1 );
115 }
116 else {
117 $messages[] = [ 'role' => 'user', 'parts' => [ [ 'text' => $query->get_message() ] ] ];
118 }
119
120 // Streamline the messages
121 $messages = $this->streamline_messages( $messages, 'model', 'parts' );
122
123 return $messages;
124 }
125
126 protected function stream_data_handler( $json ) {
127 $content = null;
128
129 // Get the content
130 if ( isset( $json['candidates'][0]['content']['parts'][0]['text'] ) ) {
131 $content = $json['candidates'][0]['content']['parts'][0]['text'];
132 }
133
134 // Avoid some endings
135 $endings = [ "<|im_end|>", "</s>" ];
136 if ( in_array( $content, $endings ) ) {
137 $content = null;
138 }
139
140 return ( $content === '0' || !empty( $content ) ) ? $content : null;
141 }
142
143 protected function build_headers( $query ) {
144 if ( $query->apiKey ) {
145 $this->apiKey = $query->apiKey;
146 }
147 if ( empty( $this->apiKey ) ) {
148 throw new Exception( 'No API Key provided. Please visit the Settings.' );
149 }
150 $headers = array(
151 'Content-Type' => 'application/json',
152 );
153 return $headers;
154 }
155
156 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
157 $body = null;
158 if ( !empty( $forms ) ) {
159 throw new Exception( 'No support for form-data requests yet.' );
160 // $boundary = wp_generate_password ( 24, false );
161 // $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
162 // $body = $this->build_form_body( $forms, $boundary );
163 }
164 else if ( !empty( $json ) ) {
165 $body = json_encode( $json );
166 }
167 $options = array(
168 'headers' => $headers,
169 'method' => $method,
170 'timeout' => MWAI_TIMEOUT,
171 'body' => $body,
172 'sslverify' => false
173 );
174 return $options;
175 }
176
177 public function run_query( $url, $options, $isStream = false ) {
178 try {
179 $options['stream'] = $isStream;
180 if ( $isStream ) {
181 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
182 }
183 $res = wp_remote_get( $url, $options );
184
185 if ( is_wp_error( $res ) ) {
186 throw new Exception( $res->get_error_message() );
187 }
188
189 if ( $isStream ) {
190 return [ 'stream' => true ];
191 }
192
193 $response = wp_remote_retrieve_body( $res );
194 $headersRes = wp_remote_retrieve_headers( $res );
195 $headers = $headersRes->getAll();
196
197 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
198 // If so, we don't need to decode the response
199 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
200 $resContentType = $normalizedHeaders['content-type'] ?? '';
201 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
202 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
203 }
204
205 $data = json_decode( $response, true );
206 $this->handle_response_errors( $data );
207 return [ 'headers' => $headers, 'data' => $data ];
208 }
209 catch ( Exception $e ) {
210 Meow_MWAI_Logging::error( "(Google) " . $e->getMessage() );
211 throw $e;
212 }
213 finally {
214 if ( $isStream && file_exists( $options['filename'] ) ) {
215 unlink( $options['filename'] );
216 }
217 }
218 }
219
220 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
221 if ( !is_null( $streamCallback ) ) {
222 $this->streamCallback = $streamCallback;
223 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
224 }
225
226 $body = array(
227 "generationConfig" => [
228 "candidateCount" => $query->maxResults,
229 "maxOutputTokens" => $query->maxTokens,
230 "temperature" => $query->temperature,
231 "stopSequences" => [],
232 ],
233 );
234
235 // if ( !empty( $query->stop ) ) {
236 // $body['generationConfig']['stop'] = $query->stop;
237 // }
238
239 // if ( !empty( $query->responseFormat ) ) {
240 // if ( $query->responseFormat === 'json' ) {
241 // $body['response_format'] = [ 'type' => 'json_object' ];
242 // }
243 // }
244
245 if ( !empty( $query->functions ) ) {
246 throw new Exception( 'AI Engine doesn\'t support Function Calling with Google models yet.' );
247 //$body['functions'] = $query->functions;
248 //$body['function_call'] = $query->functionCall;
249 }
250
251 $body['contents'] = $this->build_messages( $query );
252 $url = $this->endpoint;
253
254 // Streaming:
255 // $url .= '/models/' . $query->model . ':streamGenerateContent';
256
257 $url .= '/models/' . $query->model . ':generateContent';
258
259 // If streaming is enabled, we need to use the SSE endpoint.
260 if ( !is_null( $streamCallback ) ) {
261 $url .= '?alt=sse';
262 }
263
264 // Add the API key
265 if ( strpos( $url, '?' ) === false ) {
266 $url .= '?key=' . $this->apiKey;
267 }
268 else {
269 $url .= '&key=' . $this->apiKey;
270 }
271
272 $headers = $this->build_headers( $query );
273 $options = $this->build_options( $headers, $body );
274
275 try {
276 $res = $this->run_query( $url, $options, $streamCallback );
277 $reply = new Meow_MWAI_Reply( $query );
278
279 $returned_id = null;
280 $returned_model = $this->inModel;
281 $returned_in_tokens = null;
282 $returned_out_tokens = null;
283 $returned_choices = [];
284
285 if ( !is_null( $streamCallback ) ) {
286 // Streamed data
287 if ( empty( $this->streamContent ) ) {
288 $json = json_decode( $this->streamBuffer, true );
289 if ( isset( $json['error']['message'] ) ) {
290 throw new Exception( $json['error']['message'] );
291 }
292 }
293 $returned_id = $this->inId;
294 $returned_model = $this->inModel ? $this->inModel : $query->model;
295 $returned_choices = [
296 [
297 'message' => [
298 'content' => $this->streamContent,
299 'function_call' => $this->streamFunctionCall
300 ]
301 ]
302 ];
303 }
304 else {
305 // Regular data
306 $data = $res['data'];
307 if ( empty( $data ) ) {
308 throw new Exception( 'No content received (res is null).' );
309 }
310
311 // Not much information from Google's API :(
312 $returned_id = null;
313 $returned_model = $query->model;
314 $returned_in_tokens = null;
315 $returned_out_tokens = null;
316
317 // We should return the candidates formatted as OpenAI does it.
318 $returned_choices = [];
319 if ( isset( $data['candidates'] ) ) {
320 $candidates = $data['candidates'];
321 foreach ( $candidates as $candidate ) {
322 $content = $candidate['content'];
323 $text = $content['parts'][0]['text'];
324 $returned_choices[] = [ 'role' => 'assistant', 'text' => $text ];
325 }
326 }
327 }
328
329 // Set the results.
330 $reply->set_choices( $returned_choices );
331 if ( !empty( $returned_id ) ) {
332 $reply->set_id( $returned_id );
333 }
334
335 // Handle tokens.
336 $this->handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens );
337
338 return $reply;
339 }
340 catch ( Exception $e ) {
341 Meow_MWAI_Logging::error( "(Google) " . $e->getMessage() );
342 $message = "From Google: " . $e->getMessage();
343 throw new Exception( $message );
344 }
345 }
346
347 public function handle_tokens_usage( $reply, $query, $returned_model,
348 $returned_in_tokens, $returned_out_tokens ) {
349 $returned_in_tokens = !is_null( $returned_in_tokens ) ?
350 $returned_in_tokens : $reply->get_in_tokens( $query );
351 $returned_out_tokens = !is_null( $returned_out_tokens ) ?
352 $returned_out_tokens : $reply->get_out_tokens();
353 $usage = $this->core->record_tokens_usage(
354 $returned_model,
355 $returned_in_tokens,
356 $returned_out_tokens
357 );
358 $reply->set_usage( $usage );
359 }
360
361 /*
362 This is the rest of the OpenAI API support, not related to the models directly.
363 */
364
365 // Check if there are errors in the response from OpenAI, and throw an exception if so.
366 public function handle_response_errors( $data ) {
367 if ( isset( $data['error'] ) ) {
368 $message = $data['error']['message'];
369 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
370 $message = str_replace( $matches[1], '', $message );
371 }
372 throw new Exception( $message );
373 }
374 }
375
376 public function get_models() {
377 return $this->core->get_engine_models( 'google' );
378 }
379
380 public function retrieve_models() {
381 $url = "https://generativelanguage.googleapis.com/v1/models";
382 $url .= "?key=" . $this->apiKey;
383 $response = wp_remote_get( $url );
384 if ( is_wp_error( $response ) ) {
385 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
386 }
387 $body = json_decode( $response['body'], true );
388 $models = array();
389 foreach ( $body['models'] as $model ) {
390 if ( strpos( $model['name'], 'gemini' ) === false ) {
391 continue;
392 }
393 $family = "gemini";
394 $maxCompletionTokens = $model['outputTokenLimit'];
395 $maxContextualTokens = $model['inputTokenLimit'];
396 $priceIn = 0;
397 $priceOut = 0;
398 $tags = [ 'core', 'chat' ];
399 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
400 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'], $matches ) ) {
401 $tags[] = 'preview';
402 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
403 }
404 // If the name includes 'Vision', add 'vision' tag
405 if ( preg_match( '/vision/i', $model['name'], $matches ) ) {
406 $tags[] = 'vision';
407 }
408 else if ( preg_match( '/(vision|multimodal)/i', $model['description'], $matches ) ) {
409 $tags[] = 'vision';
410 }
411 $name = preg_replace( '/^models\//', '', $model['name'] );
412 $model = array(
413 'model' => $name,
414 'name' => $name,
415 'family' => $family,
416 'features' => ['completion'],
417 'type' => 'token',
418 'unit' => 1 / 1000,
419 'maxCompletionTokens' => $maxCompletionTokens,
420 'maxContextualTokens' => $maxContextualTokens,
421 'tags' => $tags
422 );
423 if ( $priceIn > 0 && $priceOut > 0 ) {
424 $model['price'] = array(
425 'in' => $priceIn,
426 'out' => $priceOut,
427 );
428 }
429 $models[] = $model;
430 }
431 return $models;
432 }
433
434 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
435 // TODO: Not sure how to get the price from Google's API.
436 return null;
437 }
438 }
439