PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.3
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 2 years ago core.php 1 year ago factory.php 2 years ago google.php 2 years ago huggingface.php 2 years ago openai.php 1 year ago openrouter.php 2 years ago
google.php
435 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 $this->core->log( "❌ (Google) " . $e->getMessage() );
211 throw $e;
212 }
213 }
214
215 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
216 if ( !is_null( $streamCallback ) ) {
217 $this->streamCallback = $streamCallback;
218 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
219 }
220
221 $body = array(
222 "generationConfig" => [
223 "candidateCount" => $query->maxResults,
224 "maxOutputTokens" => $query->maxTokens,
225 "temperature" => $query->temperature,
226 "stopSequences" => [],
227 ],
228 );
229
230 // if ( !empty( $query->stop ) ) {
231 // $body['generationConfig']['stop'] = $query->stop;
232 // }
233
234 // if ( !empty( $query->responseFormat ) ) {
235 // if ( $query->responseFormat === 'json' ) {
236 // $body['response_format'] = [ 'type' => 'json_object' ];
237 // }
238 // }
239
240 if ( !empty( $query->functions ) ) {
241 throw new Exception( 'AI Engine doesn\'t support Function Calling with Google models yet.' );
242 //$body['functions'] = $query->functions;
243 //$body['function_call'] = $query->functionCall;
244 }
245
246 if ( $query->mode !== 'chat' ) {
247 throw new Exception( 'Google models only support chat mode.' );
248 }
249
250 $body['contents'] = $this->build_messages( $query );
251 $url = $this->endpoint;
252
253 // Streaming:
254 // $url .= '/models/' . $query->model . ':streamGenerateContent';
255
256 $url .= '/models/' . $query->model . ':generateContent';
257
258 // If streaming is enabled, we need to use the SSE endpoint.
259 if ( !is_null( $streamCallback ) ) {
260 $url .= '?alt=sse';
261 }
262
263 // Add the API key
264 if ( strpos( $url, '?' ) === false ) {
265 $url .= '?key=' . $this->apiKey;
266 }
267 else {
268 $url .= '&key=' . $this->apiKey;
269 }
270
271 $headers = $this->build_headers( $query );
272 $options = $this->build_options( $headers, $body );
273
274 try {
275 $res = $this->run_query( $url, $options, $streamCallback );
276 $reply = new Meow_MWAI_Reply( $query );
277
278 $returned_id = null;
279 $returned_model = $this->inModel;
280 $returned_in_tokens = null;
281 $returned_out_tokens = null;
282 $returned_choices = [];
283
284 if ( !is_null( $streamCallback ) ) {
285 // Streamed data
286 if ( empty( $this->streamContent ) ) {
287 $json = json_decode( $this->streamBuffer, true );
288 if ( isset( $json['error']['message'] ) ) {
289 throw new Exception( $json['error']['message'] );
290 }
291 }
292 $returned_id = $this->inId;
293 $returned_model = $this->inModel ? $this->inModel : $query->model;
294 $returned_choices = [
295 [
296 'message' => [
297 'content' => $this->streamContent,
298 'function_call' => $this->streamFunctionCall
299 ]
300 ]
301 ];
302 }
303 else {
304 // Regular data
305 $data = $res['data'];
306 if ( empty( $data ) ) {
307 throw new Exception( 'No content received (res is null).' );
308 }
309
310 // Not much information from Google's API :(
311 $returned_id = null;
312 $returned_model = $query->model;
313 $returned_in_tokens = null;
314 $returned_out_tokens = null;
315
316 // We should return the candidates formatted as OpenAI does it.
317 $returned_choices = [];
318 if ( isset( $data['candidates'] ) ) {
319 $candidates = $data['candidates'];
320 foreach ( $candidates as $candidate ) {
321 $content = $candidate['content'];
322 $text = $content['parts'][0]['text'];
323 $returned_choices[] = [ 'role' => 'assistant', 'text' => $text ];
324 }
325 }
326 }
327
328 // Set the results.
329 $reply->set_choices( $returned_choices );
330 if ( !empty( $returned_id ) ) {
331 $reply->set_id( $returned_id );
332 }
333
334 // Handle tokens.
335 $this->handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens );
336
337 return $reply;
338 }
339 catch ( Exception $e ) {
340 $this->core->log( "❌ (Google) " . $e->getMessage() );
341 $message = "From Google: " . $e->getMessage();
342 throw new Exception( $message );
343 }
344 }
345
346 public function handle_tokens_usage( $reply, $query, $returned_model,
347 $returned_in_tokens, $returned_out_tokens ) {
348 $returned_in_tokens = !is_null( $returned_in_tokens ) ?
349 $returned_in_tokens : $reply->get_in_tokens( $query );
350 $returned_out_tokens = !is_null( $returned_out_tokens ) ?
351 $returned_out_tokens : $reply->get_out_tokens();
352 $usage = $this->core->record_tokens_usage(
353 $returned_model,
354 $returned_in_tokens,
355 $returned_out_tokens
356 );
357 $reply->set_usage( $usage );
358 }
359
360 /*
361 This is the rest of the OpenAI API support, not related to the models directly.
362 */
363
364 // Check if there are errors in the response from OpenAI, and throw an exception if so.
365 public function handle_response_errors( $data ) {
366 if ( isset( $data['error'] ) ) {
367 $message = $data['error']['message'];
368 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
369 $message = str_replace( $matches[1], '', $message );
370 }
371 throw new Exception( $message );
372 }
373 }
374
375 public function get_models() {
376 return $this->core->get_engine_models( 'google' );
377 }
378
379 public function retrieve_models() {
380 $url = "https://generativelanguage.googleapis.com/v1/models";
381 $url .= "?key=" . $this->apiKey;
382 $response = wp_remote_get( $url );
383 if ( is_wp_error( $response ) ) {
384 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
385 }
386 $body = json_decode( $response['body'], true );
387 $models = array();
388 foreach ( $body['models'] as $model ) {
389 if ( strpos( $model['name'], 'gemini' ) === false ) {
390 continue;
391 }
392 $family = "gemini";
393 $maxCompletionTokens = $model['outputTokenLimit'];
394 $maxContextualTokens = $model['inputTokenLimit'];
395 $priceIn = 0;
396 $priceOut = 0;
397 $tags = [ 'core', 'chat' ];
398 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
399 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'], $matches ) ) {
400 $tags[] = 'preview';
401 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
402 }
403 // If the name includes 'Vision', add 'vision' tag
404 if ( preg_match( '/vision/i', $model['name'], $matches ) ) {
405 $tags[] = 'vision';
406 }
407 $name = preg_replace( '/^models\//', '', $model['name'] );
408 $model = array(
409 'model' => $name,
410 'name' => $name,
411 'family' => $family,
412 'mode' => 'chat',
413 'type' => 'token',
414 'unit' => 1 / 1000,
415 'maxCompletionTokens' => $maxCompletionTokens,
416 'maxContextualTokens' => $maxContextualTokens,
417 'tags' => $tags
418 );
419 if ( $priceIn > 0 && $priceOut > 0 ) {
420 $model['price'] = array(
421 'in' => $priceIn,
422 'out' => $priceOut,
423 );
424 }
425 $models[] = $model;
426 }
427 return $models;
428 }
429
430 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
431 // TODO: Not sure how to get the price from Google's API.
432 return null;
433 }
434 }
435