PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.3.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.3.6
3.5.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 / anthropic.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
anthropic.php
451 lines
1 <?php
2
3 class Meow_MWAI_Engines_Anthropic extends Meow_MWAI_Engines_OpenAI
4 {
5 // Streaming
6 protected $streamInTokens = null;
7 protected $streamOutTokens = null;
8 protected $streamBlocks;
9 protected $streamIsThinking = false;
10
11 public function __construct( $core, $env )
12 {
13 parent::__construct( $core, $env );
14 }
15
16 public function reset_stream() {
17 $this->streamContent = null;
18 $this->streamBuffer = null;
19 $this->streamFunctionCall = null;
20 $this->streamToolCalls = [];
21 $this->streamLastMessage = null;
22 $this->streamInTokens = null;
23 $this->streamOutTokens = null;
24 $this->streamIsThinking = false;
25
26 $this->streamBlocks = [
27 'role' => 'assistant',
28 'content' => []
29 ];
30
31 $this->inModel = null;
32 $this->inId = null;
33 }
34
35 protected function set_environment() {
36 $env = $this->env;
37 $this->apiKey = $env['apikey'];
38 }
39
40 protected function build_url( $query, $endpoint = null ) {
41 $endpoint = apply_filters( 'mwai_anthropic_endpoint', 'https://api.anthropic.com/v1', $this->env );
42 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
43 $url = trailingslashit( $endpoint ) . 'messages';
44 }
45 else {
46 throw new Exception( 'AI Engine: Unsupported query type.' );
47 }
48 return $url;
49 }
50
51 protected function build_headers( $query ) {
52 parent::build_headers( $query );
53 $headers = array(
54 'Content-Type' => 'application/json',
55 'x-api-key' => $this->apiKey,
56 'anthropic-version' => '2023-06-01',
57 'anthropic-beta' => 'tools-2024-04-04',
58 'User-Agent' => 'AI Engine',
59 );
60 return $headers;
61 }
62
63 public function final_checks( Meow_MWAI_Query_Base $query ) {
64 // We skip this completely.
65 // maxMessages is handed in build_messages().
66 }
67
68 protected function build_messages( $query ) {
69 $messages = [];
70
71 // Then, if any, we need to add the 'messages', they are already formatted.
72 foreach ( $query->messages as $message ) {
73 $messages[] = $message;
74 }
75
76 // Handle the maxMessages
77 if ( !empty( $query->maxMessages ) ) {
78 $messages = array_slice( $messages, -$query->maxMessages );
79 }
80
81 // If the first message is not a 'user' role, we remove it.
82 if ( !empty( $messages ) && $messages[0]['role'] !== 'user' ) {
83 array_shift( $messages );
84 }
85
86 if ( $query->attachedFile ) {
87 // https://docs.anthropic.com/claude/reference/messages-examples#vision
88 // Claude only supports image/jpeg, image/png, image/gif, and image/webp media types.
89 $mime = $query->attachedFile->get_mimeType();
90 // Claude only supports upload by data (base64), not by URL.
91 $data = $query->attachedFile->get_base64();
92 $message = $query->get_message();
93 if ( empty( $message ) ) {
94 // Claude doesn't support messages with only images, so we add a text message.
95 $message = "I uploaded an image. Do not consider this message as part of the conversation.";
96 }
97 $messages[] = [
98 'role' => 'user',
99 'content' => [
100 [
101 "type" => "text",
102 "text" => $message
103 ],
104 [
105 "type" => "image",
106 "source" => [
107 "type" => "base64",
108 "media_type" => $mime,
109 "data" => $data
110 ]
111 ]
112 ]
113 ];
114 }
115 else {
116 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
117 }
118
119 return $messages;
120 }
121
122 // Define a function to recursively replace empty arrays with empty stdClass objects
123 // To avoid errors with OpenAI's API
124 private function replaceEmptyArrayWithObject( $item ) {
125 if ( is_array( $item ) ) {
126 if ( empty( $item ) ) {
127 return new stdClass(); // Replace empty array with empty object
128 }
129 foreach ( $item as $key => $value ) {
130 $item[$key] = $this->replaceEmptyArrayWithObject( $value ); // Recurse
131 }
132 }
133 return $item;
134 }
135
136 protected function build_body( $query, $streamCallback = null, $extra = null ) {
137 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
138 $body = array(
139 "model" => $query->model,
140 "max_tokens" => $query->maxTokens,
141 "temperature" => $query->temperature,
142 "stream" => !is_null( $streamCallback ),
143 "messages" => []
144 );
145
146 // Build the messages
147 $body['messages'][] = [ 'role' => 'user', 'content' => $query->message ];
148
149 if ( !empty( $query->blocks ) ) {
150 foreach ( $query->blocks as $feedback_block ) {
151 $body['messages'][] = [
152 'role' => 'assistant',
153 'content' => $feedback_block['rawMessage']['content']
154 ];
155 foreach ( $feedback_block['feedbacks'] as $feedback ) {
156 $feedbackValue = $feedback['reply']['value'];
157 if ( !is_string( $feedbackValue ) ) {
158 $feedbackValue = json_encode( $feedbackValue );
159 }
160 $body['messages'][] = [
161 'role' => 'user',
162 'content' => [
163 [
164 'type' => 'tool_result',
165 'tool_use_id' => $feedback['request']['toolId'],
166 'content' => $feedbackValue,
167 'is_error' => false // Cool, Anthropic supports errors!
168 ]
169 ]
170 ];
171 }
172 }
173 }
174
175 // TODO: This WAS COPIED FROM BELOW
176 // Support for functions
177 if ( !empty( $query->functions ) ) {
178 $model = $this->retrieve_model_info( $query->model );
179 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
180 $this->core->log( '⚠️ (Anthropic) The model "' . $query->model . '" doesn\'t support Function Calling.' );
181 }
182 else {
183 $body['tools'] = [];
184 // Dynamic function: they will interactively enhance the completion (tools).
185 foreach ( $query->functions as $function ) {
186 $body['tools'][] = $function->serializeForAnthropic();
187 }
188 // Static functions: they will be executed at the end of the completion.
189 //$body['function_call'] = $query->functionCall;
190 }
191 }
192
193 // To avoid errors with Anthropic's API, we need to replace empty arrays with empty objects
194 $body = $this->replaceEmptyArrayWithObject( $body );
195 return $body;
196 }
197 else if ( $query instanceof Meow_MWAI_Query_Text ) {
198 $body = array(
199 "model" => $query->model,
200 "max_tokens" => $query->maxTokens,
201 "temperature" => $query->temperature,
202 "stream" => !is_null( $streamCallback ),
203 );
204
205 if ( !empty( $query->stop ) ) {
206 $body['stop'] = $query->stop;
207 }
208
209 // First, we need to add the first message (the instructions).
210 if ( !empty( $query->instructions ) ) {
211 $body['system'] = $query->instructions;
212 }
213
214 // If there is a context, we need to add it.
215 if ( !empty( $query->context ) ) {
216 if ( empty( $body['system'] ) ) {
217 $body['system'] = "";
218 }
219 $body['system'] = empty( $body['system'] ) ? '' : $body['system'] . "\n\n";
220 $body['system'] = $body['system'] . "Context:\n\n" . $query->context;
221 }
222
223 // Support for functions
224 if ( !empty( $query->functions ) ) {
225 $model = $this->retrieve_model_info( $query->model );
226 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
227 $this->core->log( '⚠️ (Anthropic) The model "' . $query->model . '" doesn\'t support Function Calling.' );
228 }
229 else {
230 $body['tools'] = [];
231 // Dynamic function: they will interactively enhance the completion (tools).
232 foreach ( $query->functions as $function ) {
233 $body['tools'][] = $function->serializeForAnthropic();
234 }
235 // Static functions: they will be executed at the end of the completion.
236 //$body['function_call'] = $query->functionCall;
237 }
238 }
239
240 $body['messages'] = $this->build_messages( $query );
241 return $body;
242 }
243 else {
244 throw new Exception( 'AI Engine: Unsupported query type.' );
245 }
246 }
247
248 protected function stream_data_handler( $json ) {
249 $content = null;
250 $type = !empty( $json['type'] ) ? $json['type'] : null;
251 if ( is_null( $type ) ) {
252 return $content;
253 }
254
255 if ( $type === 'message_start' ) {
256 $usage = $json['message']['usage'];
257 $this->streamInTokens = $usage['input_tokens'];
258 $this->inModel = $json['message']['model'];
259 $this->inId = $json['message']['id'];
260 }
261 else if ( $type === 'content_block_start' ) {
262 $this->streamBlocks['content'][] = $json['content_block'];
263 }
264 else if ( $type === 'content_block_delta' ) {
265 $index = $json['index'];
266 $block = $this->streamBlocks['content'][$index];
267 if ( $json['delta']['type'] === 'text_delta' ) {
268 $block['text'] .= $json['delta']['text'];
269 if ( strpos( $block['text'], '<thinking' ) === 0 ) {
270 $this->streamIsThinking = true;
271 }
272 if ( strpos( $block['text'], '</thinking>' ) === 0 ) {
273 $this->streamIsThinking = false;
274 }
275 $content = $json['delta']['text'];
276 }
277 else if ( $json['delta']['type'] === 'input_json_delta' ) {
278 // Somehow, the input is set as an array, but it should be a string since it's JSON.
279 $block['input'] = is_array( $block['input'] ) ? "" : $block['input'];
280 $block['input'] .= $json['delta']['partial_json'];
281 }
282 $this->streamBlocks['content'][$index] = $block;
283 }
284 // At the end of a block, let's look for any 'input' not yet decoded from JSON
285 else if ( $type === 'content_block_stop' ) {
286 $index = $json['index'];
287 $block = $this->streamBlocks['content'][$index];
288 if ( isset( $block['input'] ) && is_string( $block['input'] ) ) {
289 $block['input'] = json_decode( $block['input'], true );
290 }
291 $this->streamBlocks['content'][$index] = $block;
292 }
293 else if ( $type === 'message_delta' ) {
294 $usage = $json['usage'];
295 $this->streamOutTokens = $usage['output_tokens'];
296 }
297 else if ( $type === 'error' ) {
298 $error = $json['error'];
299 $message = $error['message'];
300 throw new Exception( $message );
301 }
302
303 // Avoid some endings
304 $endings = [ "<|im_end|>", "</s>" ];
305 if ( in_array( $content, $endings ) ) {
306 $content = null;
307 }
308
309 // If the stream is thinking, we don't want to return anything yet.
310 if ( $this->streamIsThinking ) {
311 $content = null;
312 }
313
314 return ( $content === '0' || !empty( $content ) ) ? $content : null;
315 }
316
317 // This create the "choices" (even though, often, it is only one choice).
318 // It is basically the reply, but one that is understood by the Meow_MWAI_Reply class.
319 public function create_choices( $data ) {
320 $returned_choices = [];
321 foreach ( $data['content'] as $content ) {
322 if ( $content['type'] === 'tool_use' ) {
323 $returned_choices[] = [
324 'message' => [
325 'tool_calls' => [
326 [
327 'id' => $content['id'],
328 'type' => 'function',
329 'function' => [
330 'name' => $content['name'],
331 'arguments' => empty( $content['input'] ) ? new stdClass() : $content['input'],
332 ]
333 ]
334
335 ]
336 ]
337 ];
338 }
339 else if ( $content['type'] === 'text' ) {
340 $returned_choices[] = [
341 'message' => [
342 'content' => $content['text']
343 ]
344 ];
345 }
346 }
347 return $returned_choices;
348 }
349
350 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
351 $isStreaming = !is_null( $streamCallback );
352
353 if ( $isStreaming ) {
354 $this->streamCallback = $streamCallback;
355 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
356 }
357
358 $this->reset_stream();
359 $data = null;
360 $body = $this->build_body( $query, $streamCallback );
361 $url = $this->build_url( $query );
362 $headers = $this->build_headers( $query );
363 $options = $this->build_options( $headers, $body );
364
365 try {
366 $res = $this->run_query( $url, $options, $streamCallback );
367 $reply = new Meow_MWAI_Reply( $query );
368 $returned_id = null;
369 $returned_model = null;
370 $returned_choices = [];
371
372 // Streaming Mode
373 if ( $isStreaming ) {
374 $returned_id = $this->inId;
375 $returned_model = $this->inModel ? $this->inModel : $query->model;
376 if ( !is_null( $this->streamInTokens && !is_null( $this->streamOutTokens ) ) ) {
377 $returned_in_tokens = $this->streamInTokens;
378 $returned_out_tokens = $this->streamOutTokens;
379 }
380 $data = $this->streamBlocks;
381 $returned_choices = $this->create_choices( $this->streamBlocks );
382 }
383 // Standard Mode
384 else {
385 $data = $res['data'];
386 $returned_id = $data['id'];
387 $returned_model = $data['model'];
388 $usage = $data['usage'];
389 if ( !empty( $usage ) ) {
390 $returned_in_tokens = isset( $usage['input_tokens'] ) ? $usage['input_tokens'] : null;
391 $returned_out_tokens = isset( $usage['output_tokens'] ) ? $usage['output_tokens'] : null;
392 }
393 $returned_choices = $this->create_choices( $data );
394 }
395
396 $reply->set_choices( $returned_choices, $data );
397 if ( !empty( $returned_id ) ) {
398 $reply->set_id( $returned_id );
399 }
400
401 // Handle tokens.
402 $this->handle_tokens_usage( $reply, $query, $returned_model,
403 $returned_in_tokens, $returned_out_tokens
404 );
405
406 return $reply;
407 }
408 catch ( Exception $e ) {
409 $error = $e->getMessage();
410 $json = json_decode( $error, true );
411 if ( json_last_error() === JSON_ERROR_NONE ) {
412 if ( isset( $json['error'] ) && isset( $json['error']['message'] ) ) {
413 $error = $json['error']['message'];
414 }
415 }
416 $this->core->log( "❌ (Anthropic) $error" );
417 $service = $this->get_service_name();
418 $message = "From $service: " . $error;
419 throw new Exception( $message );
420 }
421 }
422
423 protected function get_service_name() {
424 return "Anthropic";
425 }
426
427 public function get_models() {
428 return apply_filters( 'mwai_anthropic_models', MWAI_ANTHROPIC_MODELS );
429 }
430
431 static public function get_models_static() {
432 return MWAI_ANTHROPIC_MODELS;
433 }
434
435 public function handle_tokens_usage( $reply, $query, $returned_model,
436 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
437 $returned_in_tokens = !is_null( $returned_in_tokens ) ?
438 $returned_in_tokens : $reply->get_in_tokens( $query );
439 $returned_out_tokens = !is_null( $returned_out_tokens ) ?
440 $returned_out_tokens : $reply->get_out_tokens();
441 if ( !empty( $reply->id ) ) {
442 // Would be cool to retrieve the usage from the API, but it's not possible.
443 }
444 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
445 $reply->set_usage( $usage );
446 }
447
448 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
449 return parent::get_price( $query, $reply );
450 }
451 }