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