PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.54
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.54
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 2 years ago factory.php 2 years ago google.php 2 years ago huggingface.php 2 years ago openai.php 2 years ago openrouter.php 2 years ago
google.php
445 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 $fileUrl = $query->get_file_url();
94 if ( !empty( $fileUrl ) ) {
95 // If the fileUrl actually is data (starts with "data:")
96 $isData = strpos( $fileUrl, 'data:' ) === 0;
97 if ( $isData ) {
98 $messages[] = [
99 'role' => 'user',
100 'parts' => [
101 [
102 "inlineData" => [
103 "mimeType" => "image/jpeg",
104 "data" => $query->file // We need to be careful here to get only the data part
105 ]
106 ],
107 [
108 "text" => $query->get_message()
109 ]
110 ]
111 ];
112 }
113 else {
114 $messages[] = [
115 'role' => 'user',
116 'parts' => [
117 [
118 "fileData" => [
119 "mimeType" => "image/jpeg",
120 "fileUri" => $fileUrl
121 ]
122 ],
123 [
124 "text" => $query->get_message()
125 ]
126 ]
127 ];
128 }
129 // TODO: Gemini doesn't support multiturn chat with Vision...
130 // So we only keep the message that goes with the image.
131 $messages = array_slice( $messages, -1 );
132 }
133 else {
134 $messages[] = [ 'role' => 'user', 'parts' => [ [ 'text' => $query->get_message() ] ] ];
135 }
136
137 // Streamline the messages
138 $messages = $this->streamline_messages( $messages, 'model', 'parts' );
139
140 return $messages;
141 }
142
143 protected function stream_data_handler( $json ) {
144 $content = null;
145
146 // Get the content
147 if ( isset( $json['candidates'][0]['content']['parts'][0]['text'] ) ) {
148 $content = $json['candidates'][0]['content']['parts'][0]['text'];
149 }
150
151 // Avoid some endings
152 $endings = [ "<|im_end|>", "</s>" ];
153 if ( in_array( $content, $endings ) ) {
154 $content = null;
155 }
156
157 return ( $content === '0' || !empty( $content ) ) ? $content : null;
158 }
159
160 protected function build_headers( $query ) {
161 if ( $query->apiKey ) {
162 $this->apiKey = $query->apiKey;
163 }
164 if ( empty( $this->apiKey ) ) {
165 throw new Exception( 'No API Key provided. Please visit the Settings.' );
166 }
167 $headers = array(
168 'Content-Type' => 'application/json',
169 );
170 return $headers;
171 }
172
173 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
174 $body = null;
175 if ( !empty( $forms ) ) {
176 throw new Exception( 'No support for form-data requests yet.' );
177 // $boundary = wp_generate_password ( 24, false );
178 // $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
179 // $body = $this->build_form_body( $forms, $boundary );
180 }
181 else if ( !empty( $json ) ) {
182 $body = json_encode( $json );
183 }
184 $options = array(
185 'headers' => $headers,
186 'method' => $method,
187 'timeout' => MWAI_TIMEOUT,
188 'body' => $body,
189 'sslverify' => false
190 );
191 return $options;
192 }
193
194 public function run_query( $url, $options, $isStream = false ) {
195 try {
196 $options['stream'] = $isStream;
197 if ( $isStream ) {
198 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
199 }
200 $res = wp_remote_get( $url, $options );
201
202 if ( is_wp_error( $res ) ) {
203 throw new Exception( $res->get_error_message() );
204 }
205
206 if ( $isStream ) {
207 return [ 'stream' => true ];
208 }
209
210 $response = wp_remote_retrieve_body( $res );
211 $headersRes = wp_remote_retrieve_headers( $res );
212 $headers = $headersRes->getAll();
213
214 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
215 // If so, we don't need to decode the response
216 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
217 $resContentType = $normalizedHeaders['content-type'] ?? '';
218 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
219 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
220 }
221
222 $data = json_decode( $response, true );
223 $this->handle_response_errors( $data );
224 return [ 'headers' => $headers, 'data' => $data ];
225 }
226 catch ( Exception $e ) {
227 error_log( $e->getMessage() );
228 throw $e;
229 }
230 }
231
232 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
233 if ( !is_null( $streamCallback ) ) {
234 $this->streamCallback = $streamCallback;
235 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
236 }
237
238 $body = array(
239 "generationConfig" => [
240 "candidateCount" => $query->maxResults,
241 "maxOutputTokens" => $query->maxTokens,
242 "temperature" => $query->temperature,
243 "stopSequences" => [],
244 ],
245 );
246
247 // if ( !empty( $query->stop ) ) {
248 // $body['generationConfig']['stop'] = $query->stop;
249 // }
250
251 // if ( !empty( $query->responseFormat ) ) {
252 // if ( $query->responseFormat === 'json' ) {
253 // $body['response_format'] = [ 'type' => 'json_object' ];
254 // }
255 // }
256
257 if ( !empty( $query->functions ) ) {
258 throw new Exception( 'AI Engine doesn\'t support Function Calling with Google models yet.' );
259 //$body['functions'] = $query->functions;
260 //$body['function_call'] = $query->functionCall;
261 }
262
263 if ( $query->mode !== 'chat' ) {
264 throw new Exception( 'Google models only support chat mode.' );
265 }
266
267 $body['contents'] = $this->build_messages( $query );
268 $url = $this->endpoint;
269
270 // Streaming:
271 // $url .= '/models/' . $query->model . ':streamGenerateContent';
272
273 $url .= '/models/' . $query->model . ':generateContent';
274
275 // If streaming is enabled, we need to use the SSE endpoint.
276 if ( !is_null( $streamCallback ) ) {
277 $url .= '?alt=sse';
278 }
279
280 // Add the API key
281 if ( strpos( $url, '?' ) === false ) {
282 $url .= '?key=' . $this->apiKey;
283 }
284 else {
285 $url .= '&key=' . $this->apiKey;
286 }
287
288 $headers = $this->build_headers( $query );
289 $options = $this->build_options( $headers, $body );
290
291 try {
292 $res = $this->run_query( $url, $options, $streamCallback );
293 $reply = new Meow_MWAI_Reply( $query );
294
295 $returned_id = null;
296 $returned_model = $this->inModel;
297 $returned_in_tokens = null;
298 $returned_out_tokens = null;
299 $returned_choices = [];
300
301 if ( !is_null( $streamCallback ) ) {
302 // Streamed data
303 if ( empty( $this->streamContent ) ) {
304 $json = json_decode( $this->streamBuffer, true );
305 if ( isset( $json['error']['message'] ) ) {
306 throw new Exception( $json['error']['message'] );
307 }
308 }
309 $returned_id = $this->inId;
310 $returned_model = $this->inModel ? $this->inModel : $query->model;
311 $returned_choices = [
312 [
313 'message' => [
314 'content' => $this->streamContent,
315 'function_call' => $this->streamFunctionCall
316 ]
317 ]
318 ];
319 }
320 else {
321 // Regular data
322 $data = $res['data'];
323 if ( empty( $data ) ) {
324 throw new Exception( 'No content received (res is null).' );
325 }
326
327 // Not much information from Google's API :(
328 $returned_id = null;
329 $returned_model = $query->model;
330 $returned_in_tokens = 0;
331 $returned_out_tokens = 0;
332
333 // We should return the candidates formatted as OpenAI does it.
334 $returned_choices = [];
335 if ( isset( $data['candidates'] ) ) {
336 $candidates = $data['candidates'];
337 foreach ( $candidates as $candidate ) {
338 $content = $candidate['content'];
339 $text = $content['parts'][0]['text'];
340 $returned_choices[] = [ 'role' => 'assistant', 'text' => $text ];
341 }
342 }
343 }
344
345 // Set the results.
346 $reply->set_choices( $returned_choices );
347 if ( !empty( $returned_id ) ) {
348 $reply->set_id( $returned_id );
349 }
350
351 // Handle tokens.
352 $this->handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens );
353
354 return $reply;
355 }
356 catch ( Exception $e ) {
357 error_log( $e->getMessage() );
358 $message = "From Google: " . $e->getMessage();
359 throw new Exception( $message );
360 }
361 }
362
363 public function handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens ) {
364 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens : $reply->get_in_tokens( $query );
365 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens : $reply->get_out_tokens();
366 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
367 $reply->set_usage( $usage );
368 }
369
370 /*
371 This is the rest of the OpenAI API support, not related to the models directly.
372 */
373
374 // Check if there are errors in the response from OpenAI, and throw an exception if so.
375 public function handle_response_errors( $data ) {
376 if ( isset( $data['error'] ) ) {
377 $message = $data['error']['message'];
378 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
379 $message = str_replace( $matches[1], '', $message );
380 }
381 throw new Exception( $message );
382 }
383 }
384
385 public function get_models() {
386 return $this->core->get_option( 'google_models' );
387 }
388
389 public function retrieve_models() {
390 $url = "https://generativelanguage.googleapis.com/v1/models";
391 $url .= "?key=" . $this->apiKey;
392 $response = wp_remote_get( $url );
393 if ( is_wp_error( $response ) ) {
394 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
395 }
396 $body = json_decode( $response['body'], true );
397 $models = array();
398 foreach ( $body['models'] as $model ) {
399 if ( strpos( $model['name'], 'gemini' ) === false ) {
400 continue;
401 }
402 $family = "gemini";
403 $maxCompletionTokens = $model['outputTokenLimit'];
404 $maxContextualTokens = $model['inputTokenLimit'];
405 $priceIn = 0;
406 $priceOut = 0;
407 $tags = [ 'core', 'chat' ];
408 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
409 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'], $matches ) ) {
410 $tags[] = 'preview';
411 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
412 }
413 // If the name includes 'Vision', add 'vision' tag
414 if ( preg_match( '/vision/i', $model['name'], $matches ) ) {
415 $tags[] = 'vision';
416 }
417 $name = preg_replace( '/^models\//', '', $model['name'] );
418 $model = array(
419 'model' => $name,
420 'name' => $name,
421 'family' => $family,
422 'mode' => 'chat',
423 'type' => 'token',
424 'unit' => 1 / 1000,
425 'maxCompletionTokens' => $maxCompletionTokens,
426 'maxContextualTokens' => $maxContextualTokens,
427 'tags' => $tags
428 );
429 if ( $priceIn > 0 && $priceOut > 0 ) {
430 $model['price'] = array(
431 'in' => $priceIn,
432 'out' => $priceOut,
433 );
434 }
435 $models[] = $model;
436 }
437 return $models;
438 }
439
440 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
441 // TODO: Not sure how to get the price from Google's API.
442 return null;
443 }
444 }
445