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