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