PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.0
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 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
anthropic.php
472 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 $contentBlock = $feedback_block['rawMessage']['content'];
152 $assistantMessageIndex = count($body['messages']);
153 $body['messages'][] = [
154 'role' => 'assistant',
155 'content' => $contentBlock
156 ];
157
158 foreach ( $feedback_block['feedbacks'] as $feedback ) {
159 $feedbackValue = $feedback['reply']['value'];
160 if ( !is_string( $feedbackValue ) ) {
161 $feedbackValue = json_encode( $feedbackValue );
162 }
163
164 // Check for the tool_use message and make sure input is not null.
165 foreach ( $body['messages'][$assistantMessageIndex]['content'] as &$message ) {
166 if ( $message['type'] === 'tool_use' && $message['id'] === $feedback['request']['toolId'] ) {
167 if ( empty( $message['input'] ) ) {
168 $message['input'] = new stdClass();
169 }
170 break;
171 }
172 }
173 unset( $message );
174
175 // Add a new message with tool_result if tool_use.
176 $body['messages'][] = [
177 'role' => 'user',
178 'content' => [
179 [
180 'type' => 'tool_result',
181 'tool_use_id' => $feedback['request']['toolId'],
182 'content' => $feedbackValue,
183 'is_error' => false // Cool, Anthropic supports errors!
184 ]
185 ]
186 ];
187 }
188 }
189 }
190
191 // TODO: This WAS COPIED FROM BELOW
192 // Support for functions
193 if ( !empty( $query->functions ) ) {
194 $model = $this->retrieve_model_info( $query->model );
195 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
196 $this->core->log( '⚠️ (Anthropic) The model "' . $query->model . '" doesn\'t support Function Calling.' );
197 }
198 else {
199 $body['tools'] = [];
200 // Dynamic function: they will interactively enhance the completion (tools).
201 foreach ( $query->functions as $function ) {
202 $body['tools'][] = $function->serializeForAnthropic();
203 }
204 // Static functions: they will be executed at the end of the completion.
205 //$body['function_call'] = $query->functionCall;
206 }
207 }
208
209 // To avoid errors with Anthropic's API, we need to replace empty arrays with empty objects
210 $body = $this->replaceEmptyArrayWithObject( $body );
211 return $body;
212 }
213 else if ( $query instanceof Meow_MWAI_Query_Text ) {
214 $body = array(
215 "model" => $query->model,
216 "max_tokens" => $query->maxTokens,
217 "temperature" => $query->temperature,
218 "stream" => !is_null( $streamCallback ),
219 );
220
221 if ( !empty( $query->stop ) ) {
222 $body['stop'] = $query->stop;
223 }
224
225 // First, we need to add the first message (the instructions).
226 if ( !empty( $query->instructions ) ) {
227 $body['system'] = $query->instructions;
228 }
229
230 // If there is a context, we need to add it.
231 if ( !empty( $query->context ) ) {
232 if ( empty( $body['system'] ) ) {
233 $body['system'] = "";
234 }
235 $body['system'] = empty( $body['system'] ) ? '' : $body['system'] . "\n\n";
236 $body['system'] = $body['system'] . "Context:\n\n" . $query->context;
237 }
238
239 // Support for functions
240 if ( !empty( $query->functions ) ) {
241 $model = $this->retrieve_model_info( $query->model );
242 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
243 $this->core->log( '⚠️ (Anthropic) The model "' . $query->model . '" doesn\'t support Function Calling.' );
244 }
245 else {
246 $body['tools'] = [];
247 // Dynamic function: they will interactively enhance the completion (tools).
248 foreach ( $query->functions as $function ) {
249 $body['tools'][] = $function->serializeForAnthropic();
250 }
251 // Static functions: they will be executed at the end of the completion.
252 //$body['function_call'] = $query->functionCall;
253 }
254 }
255
256 $body['messages'] = $this->build_messages( $query );
257 return $body;
258 }
259 else {
260 throw new Exception( 'AI Engine: Unsupported query type.' );
261 }
262 }
263
264 protected function stream_data_handler( $json ) {
265 $content = null;
266 $type = !empty( $json['type'] ) ? $json['type'] : null;
267 if ( is_null( $type ) ) {
268 return $content;
269 }
270
271 if ( $type === 'message_start' ) {
272 $usage = $json['message']['usage'];
273 $this->streamInTokens = $usage['input_tokens'];
274 $this->inModel = $json['message']['model'];
275 $this->inId = $json['message']['id'];
276 }
277 else if ( $type === 'content_block_start' ) {
278 $this->streamBlocks['content'][] = $json['content_block'];
279 }
280 else if ( $type === 'content_block_delta' ) {
281 $index = $json['index'];
282 $block = $this->streamBlocks['content'][$index];
283 if ( $json['delta']['type'] === 'text_delta' ) {
284 $block['text'] .= $json['delta']['text'];
285 if ( strpos( $block['text'], '<thinking' ) === 0 ) {
286 $this->streamIsThinking = true;
287 }
288 if ( strpos( $block['text'], '</thinking>' ) === 0 ) {
289 $this->streamIsThinking = false;
290 }
291 $content = $json['delta']['text'];
292 }
293 else if ( $json['delta']['type'] === 'input_json_delta' ) {
294 // Somehow, the input is set as an array, but it should be a string since it's JSON.
295 $block['input'] = is_array( $block['input'] ) ? "" : $block['input'];
296 $block['input'] .= $json['delta']['partial_json'];
297 }
298 $this->streamBlocks['content'][$index] = $block;
299 }
300 // At the end of a block, let's look for any 'input' not yet decoded from JSON
301 else if ( $type === 'content_block_stop' ) {
302 $index = $json['index'];
303 $block = $this->streamBlocks['content'][$index];
304 if ( isset( $block['input'] ) && is_string( $block['input'] ) ) {
305 $block['input'] = json_decode( $block['input'], true );
306 }
307 $this->streamBlocks['content'][$index] = $block;
308 }
309 else if ( $type === 'message_delta' ) {
310 $usage = $json['usage'];
311 $this->streamOutTokens = $usage['output_tokens'];
312 }
313 else if ( $type === 'error' ) {
314 $error = $json['error'];
315 $message = $error['message'];
316 throw new Exception( $message );
317 }
318
319 // Avoid some endings
320 $endings = [ "<|im_end|>", "</s>" ];
321 if ( in_array( $content, $endings ) ) {
322 $content = null;
323 }
324
325 // If the stream is thinking, we don't want to return anything yet.
326 if ( $this->streamIsThinking ) {
327 $content = null;
328 }
329
330 return ( $content === '0' || !empty( $content ) ) ? $content : null;
331 }
332
333 // This create the "choices" (even though, often, it is only one choice).
334 // It is basically the reply, but one that is understood by the Meow_MWAI_Reply class.
335 public function create_choices( $data ) {
336 $returned_choices = [];
337 foreach ( $data['content'] as $content ) {
338 if ( $content['type'] === 'tool_use' ) {
339 $returned_choices[] = [
340 'message' => [
341 'tool_calls' => [
342 [
343 'id' => $content['id'],
344 'type' => 'function',
345 'function' => [
346 'name' => $content['name'],
347 'arguments' => empty( $content['input'] ) ? new stdClass() : $content['input'],
348 ]
349 ]
350
351 ]
352 ]
353 ];
354 }
355 else if ( $content['type'] === 'text' ) {
356 $returned_choices[] = [
357 'message' => [
358 'content' => $content['text']
359 ]
360 ];
361 }
362 }
363 return $returned_choices;
364 }
365
366 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
367 $isStreaming = !is_null( $streamCallback );
368
369 if ( $isStreaming ) {
370 $this->streamCallback = $streamCallback;
371 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
372 }
373
374 $this->reset_stream();
375 $data = null;
376 $body = $this->build_body( $query, $streamCallback );
377 $url = $this->build_url( $query );
378 $headers = $this->build_headers( $query );
379 $options = $this->build_options( $headers, $body );
380
381 try {
382 $res = $this->run_query( $url, $options, $streamCallback );
383 $reply = new Meow_MWAI_Reply( $query );
384 $returned_id = null;
385 $returned_model = null;
386 $returned_choices = [];
387
388 // Streaming Mode
389 if ( $isStreaming ) {
390 $returned_id = $this->inId;
391 $returned_model = $this->inModel ? $this->inModel : $query->model;
392 if ( !is_null( $this->streamInTokens && !is_null( $this->streamOutTokens ) ) ) {
393 $returned_in_tokens = $this->streamInTokens;
394 $returned_out_tokens = $this->streamOutTokens;
395 }
396 $data = $this->streamBlocks;
397 $returned_choices = $this->create_choices( $this->streamBlocks );
398 }
399 // Standard Mode
400 else {
401 $data = $res['data'];
402 $returned_id = $data['id'];
403 $returned_model = $data['model'];
404 $usage = $data['usage'];
405 if ( !empty( $usage ) ) {
406 $returned_in_tokens = isset( $usage['input_tokens'] ) ? $usage['input_tokens'] : null;
407 $returned_out_tokens = isset( $usage['output_tokens'] ) ? $usage['output_tokens'] : null;
408 }
409 $returned_choices = $this->create_choices( $data );
410 }
411
412 $reply->set_choices( $returned_choices, $data );
413 if ( !empty( $returned_id ) ) {
414 $reply->set_id( $returned_id );
415 }
416
417 // Handle tokens.
418 $this->handle_tokens_usage( $reply, $query, $returned_model,
419 $returned_in_tokens, $returned_out_tokens
420 );
421
422 return $reply;
423 }
424 catch ( Exception $e ) {
425 $error = $e->getMessage();
426 $json = json_decode( $error, true );
427 if ( json_last_error() === JSON_ERROR_NONE ) {
428 if ( isset( $json['error'] ) && isset( $json['error']['message'] ) ) {
429 $error = $json['error']['message'];
430 }
431 }
432 $this->core->log( "❌ (Anthropic) $error" );
433 $service = $this->get_service_name();
434 $message = "From $service: " . $error;
435 throw new Exception( $message );
436 }
437 finally {
438 if ( $isStreaming ) {
439 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
440 }
441 }
442 }
443
444 protected function get_service_name() {
445 return "Anthropic";
446 }
447
448 public function get_models() {
449 return apply_filters( 'mwai_anthropic_models', MWAI_ANTHROPIC_MODELS );
450 }
451
452 static public function get_models_static() {
453 return MWAI_ANTHROPIC_MODELS;
454 }
455
456 public function handle_tokens_usage( $reply, $query, $returned_model,
457 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
458 $returned_in_tokens = !is_null( $returned_in_tokens ) ?
459 $returned_in_tokens : $reply->get_in_tokens( $query );
460 $returned_out_tokens = !is_null( $returned_out_tokens ) ?
461 $returned_out_tokens : $reply->get_out_tokens();
462 if ( !empty( $reply->id ) ) {
463 // Would be cool to retrieve the usage from the API, but it's not possible.
464 }
465 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
466 $reply->set_usage( $usage );
467 }
468
469 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
470 return parent::get_price( $query, $reply );
471 }
472 }