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