PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.4
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
traits 1 year ago anthropic.php 10 months ago chatml.php 10 months ago core.php 11 months ago factory.php 1 year ago google.php 10 months ago hugging-face.php 1 year ago open-router.php 11 months ago openai.php 10 months ago perplexity.php 10 months ago replicate.php 1 year ago
anthropic.php
923 lines
1 <?php
2
3 class Meow_MWAI_Engines_Anthropic extends Meow_MWAI_Engines_ChatML {
4 // Streaming
5 protected $streamInTokens = null;
6 protected $streamOutTokens = null;
7 protected $streamBlocks;
8 protected $streamIsThinking = false;
9 protected $mcpServerNames = [];
10 protected $mcpTools = []; // Track MCP tools by ID
11 protected $mcpToolCount = 0;
12 protected $textStarted = false; // Track if text streaming has started
13 protected $requestSentEmitted = false; // Track if request sent event was emitted
14
15 public function __construct( $core, $env ) {
16 parent::__construct( $core, $env );
17 }
18
19 protected function isMCPTool( $toolName ) {
20 // Get all MCP tools from the filter
21 $mcpTools = apply_filters( 'mwai_mcp_tools', [] );
22
23 // Log available MCP tools for debugging
24 if ( empty( $mcpTools ) ) {
25 Meow_MWAI_Logging::log( 'Anthropic: No MCP tools available from filter' );
26 }
27
28 foreach ( $mcpTools as $tool ) {
29 if ( isset( $tool['name'] ) && $tool['name'] === $toolName ) {
30 Meow_MWAI_Logging::log( "Anthropic: Found MCP tool match: {$toolName}" );
31 return true;
32 }
33 }
34
35 // If we have MCP servers but tool not found, it might be an issue
36 if ( !empty( $this->mcpServerNames ) && !empty( $toolName ) ) {
37 Meow_MWAI_Logging::log( "Anthropic: Tool '{$toolName}' not found in MCP tools list" );
38 }
39
40 return false;
41 }
42
43 public function reset_stream() {
44 $this->streamContent = null;
45 $this->streamBuffer = null;
46 $this->streamFunctionCall = null;
47 $this->streamToolCalls = [];
48 $this->streamLastMessage = null;
49 $this->streamInTokens = null;
50 $this->streamOutTokens = null;
51 $this->streamIsThinking = false;
52 $this->mcpTools = []; // Reset MCP tools tracking
53 $this->textStarted = false; // Reset text started flag
54 $this->requestSentEmitted = false; // Reset request sent flag
55 $this->emittedFunctionResults = []; // Reset function result tracking
56
57 $this->streamBlocks = [
58 'role' => 'assistant',
59 'content' => []
60 ];
61
62 $this->inModel = null;
63 $this->inId = null;
64 }
65
66 protected function set_environment() {
67 $env = $this->env;
68 $this->apiKey = $env['apikey'];
69 }
70
71 protected function build_url( $query, $endpoint = null ) {
72 $endpoint = apply_filters( 'mwai_anthropic_endpoint', 'https://api.anthropic.com/v1', $this->env );
73 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
74 $url = trailingslashit( $endpoint ) . 'messages';
75 }
76 else {
77 throw new Exception( 'AI Engine: Unsupported query type.' );
78 }
79 return $url;
80 }
81
82 protected function build_headers( $query ) {
83 parent::build_headers( $query );
84 $headers = [
85 'Content-Type' => 'application/json',
86 'x-api-key' => $this->apiKey,
87 'anthropic-version' => '2023-06-01',
88 'anthropic-beta' => 'tools-2024-04-04, pdfs-2024-09-25, mcp-client-2025-04-04',
89 'User-Agent' => 'AI Engine',
90 ];
91 return $headers;
92 }
93
94 public function final_checks( Meow_MWAI_Query_Base $query ) {
95 // We skip this completely.
96 // maxMessages is handed in build_messages().
97 }
98
99 protected function build_messages( $query ) {
100 $messages = [];
101
102 // Then, if any, we need to add the 'messages', they are already formatted.
103 foreach ( $query->messages as $message ) {
104 $messages[] = $message;
105 }
106
107 // Handle the maxMessages
108 if ( !empty( $query->maxMessages ) ) {
109 $messages = array_slice( $messages, -$query->maxMessages );
110 }
111
112 // If the first message is not a 'user' role, we remove it.
113 if ( !empty( $messages ) && $messages[0]['role'] !== 'user' ) {
114 array_shift( $messages );
115 }
116
117 if ( $query->attachedFile ) {
118 // https://docs.anthropic.com/claude/reference/messages-examples#vision
119 // Claude only supports image/jpeg, image/png, image/gif, and image/webp media types.
120 $mime = $query->attachedFile->get_mimeType();
121 // Claude only supports upload by data (base64), not by URL.
122 $data = $query->attachedFile->get_base64();
123 $message = $query->get_message();
124 $isPDF = $mime === 'application/pdf';
125 $isIMG = !$isPDF && $query->attachedFile->is_image();
126
127 if ( $isPDF ) {
128 if ( empty( $message ) ) {
129 // Claude doesn't support messages with only PDFs, so we add a text message.
130 $message = 'I uploaded a PDF. Do not consider this message as part of the conversation.';
131 }
132 $messages[] = [
133 'role' => 'user',
134 'content' => [
135 [
136 'type' => 'text',
137 'text' => $message
138 ],
139 [
140 'type' => 'document',
141 'source' => [
142 'type' => 'base64',
143 'media_type' => 'application/pdf',
144 'data' => $data
145 ]
146 ]
147 ]
148 ];
149 }
150 else if ( $isIMG ) {
151 if ( empty( $message ) ) {
152 // Claude doesn't support messages with only images, so we add a text message.
153 $message = 'I uploaded an image. Do not consider this message as part of the conversation.';
154 }
155 $messages[] = [
156 'role' => 'user',
157 'content' => [
158 [
159 'type' => 'text',
160 'text' => $message
161 ],
162 [
163 'type' => 'image',
164 'source' => [
165 'type' => 'base64',
166 'media_type' => $mime,
167 'data' => $data
168 ]
169 ]
170 ]
171 ];
172 }
173 }
174 else {
175 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
176 }
177
178 return $messages;
179 }
180
181 // Define a function to recursively replace empty arrays with empty stdClass objects
182 // To avoid errors with OpenAI's API
183 private function replaceEmptyArrayWithObject( $item ) {
184 if ( is_array( $item ) ) {
185 if ( empty( $item ) ) {
186 return new stdClass(); // Replace empty array with empty object
187 }
188 foreach ( $item as $key => $value ) {
189 $item[$key] = $this->replaceEmptyArrayWithObject( $value ); // Recurse
190 }
191 }
192 return $item;
193 }
194
195 protected function build_body( $query, $streamCallback = null, $extra = null ) {
196 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
197 $body = [
198 'model' => $query->model,
199 'max_tokens' => $query->maxTokens,
200 'temperature' => $query->temperature,
201 'stream' => !is_null( $streamCallback ),
202 'messages' => []
203 ];
204
205 if ( !empty( $query->instructions ) ) {
206 $body['system'] = $query->instructions;
207 }
208
209 // Build the messages
210 $body['messages'][] = [ 'role' => 'user', 'content' => $query->message ];
211
212 if ( !empty( $query->blocks ) ) {
213 foreach ( $query->blocks as $feedback_block ) {
214 $contentBlock = $feedback_block['rawMessage']['content'];
215
216 // Process each content item individually to ensure proper handling of multiple tool_use blocks
217 if ( is_array( $contentBlock ) ) {
218 foreach ( $contentBlock as &$contentItem ) {
219 if ( isset( $contentItem['type'] ) && $contentItem['type'] === 'tool_use' ) {
220 // Debug logging for tool_use blocks
221 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
222 error_log( 'AI Engine: Anthropic tool_use block - ID: ' . ( $contentItem['id'] ?? 'unknown' ) .
223 ', Name: ' . ( $contentItem['name'] ?? 'unknown' ) .
224 ', Input type: ' . gettype( $contentItem['input'] ?? null ) .
225 ', Input value: ' . json_encode( $contentItem['input'] ?? null ) );
226 }
227
228 // Ensure input is an object, not an array
229 if ( isset( $contentItem['input'] ) ) {
230 if ( empty( $contentItem['input'] ) || ( is_array( $contentItem['input'] ) && count( $contentItem['input'] ) === 0 ) ) {
231 $contentItem['input'] = new stdClass();
232 } else {
233 // Apply replaceEmptyArrayWithObject only to the input field
234 $contentItem['input'] = $this->replaceEmptyArrayWithObject( $contentItem['input'] );
235 }
236 } else {
237 $contentItem['input'] = new stdClass();
238 }
239
240 // Debug logging after conversion
241 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
242 error_log( 'AI Engine: After conversion - Input type: ' . gettype( $contentItem['input'] ) .
243 ', Input value: ' . json_encode( $contentItem['input'] ) );
244 }
245 }
246 }
247 unset( $contentItem );
248 }
249
250 // Final debug logging before adding the message
251 if ( $this->core->get_option( 'queries_debug_mode' ) && is_array( $contentBlock ) ) {
252 error_log( 'AI Engine: Final contentBlock being added to messages: ' . json_encode( $contentBlock ) );
253 }
254
255 $assistantMessageIndex = count( $body['messages'] );
256 $body['messages'][] = [
257 'role' => 'assistant',
258 'content' => $contentBlock
259 ];
260
261 // Collect all tool results for this message
262 $toolResults = [];
263
264 foreach ( $feedback_block['feedbacks'] as $feedback ) {
265 $feedbackValue = $feedback['reply']['value'];
266 if ( !is_string( $feedbackValue ) ) {
267 $feedbackValue = json_encode( $feedbackValue );
268 }
269
270 $toolResults[] = [
271 'type' => 'tool_result',
272 'tool_use_id' => $feedback['request']['toolId'],
273 'content' => [
274 [
275 'type' => 'text',
276 'text' => $feedbackValue
277 ]
278 ],
279 'is_error' => false // Cool, Anthropic supports errors!
280 ];
281
282 // Note: Function result events are now emitted centrally in core.php
283 // when the function is actually executed
284 }
285
286 // Add all tool results in a single user message
287 // Anthropic requires all tool_results for a message to be in one content array
288 if ( !empty( $toolResults ) ) {
289 $body['messages'][] = [
290 'role' => 'user',
291 'content' => $toolResults
292 ];
293 }
294 }
295 }
296
297 // TODO: This WAS COPIED FROM BELOW
298 // Support for functions
299 if ( !empty( $query->functions ) ) {
300 $model = $this->retrieve_model_info( $query->model );
301 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
302 Meow_MWAI_Logging::warn( 'The model "' . $query->model . '" doesn\'t support Function Calling.' );
303 }
304 else {
305 $body['tools'] = [];
306 // Dynamic function: they will interactively enhance the completion (tools).
307 foreach ( $query->functions as $function ) {
308 $body['tools'][] = $function->serializeForAnthropic();
309 }
310 // Static functions: they will be executed at the end of the completion.
311 //$body['function_call'] = $query->functionCall;
312 }
313 }
314
315 // To avoid errors with Anthropic's API, we need to replace empty arrays with empty objects
316 // Note: We've already handled tool_use inputs above, so no need to process them again
317 return $body;
318 }
319 else if ( $query instanceof Meow_MWAI_Query_Text ) {
320 $body = [
321 'model' => $query->model,
322 'stream' => !is_null( $streamCallback ),
323 ];
324
325 if ( !empty( $query->maxTokens ) ) {
326 $body['max_tokens'] = $query->maxTokens;
327 }
328 else {
329 // https://docs.anthropic.com/en/docs/about-claude/models#model-comparison-table
330 $body['max_tokens'] = 4096;
331 }
332
333 if ( !empty( $query->temperature ) ) {
334 $body['temperature'] = $query->temperature;
335 }
336
337 if ( !empty( $query->stop ) ) {
338 $body['stop'] = $query->stop;
339 }
340
341 // First, we need to add the first message (the instructions).
342 if ( !empty( $query->instructions ) ) {
343 $body['system'] = $query->instructions;
344 }
345
346 // If there is a context, we need to add it.
347 if ( !empty( $query->context ) ) {
348 if ( empty( $body['system'] ) ) {
349 $body['system'] = '';
350 }
351 $body['system'] = empty( $body['system'] ) ? '' : $body['system'] . "\n\n";
352 $body['system'] = $body['system'] . "Context:\n\n" . $query->context;
353 }
354
355 // Support for functions
356 if ( !empty( $query->functions ) ) {
357 $model = $this->retrieve_model_info( $query->model );
358 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
359 Meow_MWAI_Logging::warn( 'The model "' . $query->model . '" doesn\'t support Function Calling.' );
360 }
361 else {
362 $body['tools'] = [];
363 // Dynamic function: they will interactively enhance the completion (tools).
364 foreach ( $query->functions as $function ) {
365 $body['tools'][] = $function->serializeForAnthropic();
366 }
367 // Static functions: they will be executed at the end of the completion.
368 //$body['function_call'] = $query->functionCall;
369 }
370 }
371
372 $body['messages'] = $this->build_messages( $query );
373
374 // Add MCP servers if available
375 if ( isset( $query->mcpServers ) && is_array( $query->mcpServers ) && !empty( $query->mcpServers ) ) {
376 $body['mcp_servers'] = [];
377 $mcp_envs = $this->core->get_option( 'mcp_envs' );
378 $this->mcpServerNames = []; // Reset MCP server names
379
380 foreach ( $query->mcpServers as $mcpServer ) {
381 if ( isset( $mcpServer['id'] ) ) {
382 // Find the full MCP server configuration by ID
383 foreach ( $mcp_envs as $env ) {
384 if ( $env['id'] === $mcpServer['id'] ) {
385 $mcp_config = [
386 'type' => 'url',
387 'url' => $env['url'],
388 'name' => $env['name'],
389 'tool_configuration' => [
390 'enabled' => true
391 ]
392 ];
393
394 // Add authorization token if available
395 if ( !empty( $env['token'] ) ) {
396 $mcp_config['authorization_token'] = $env['token'];
397 }
398
399 $body['mcp_servers'][] = $mcp_config;
400 $this->mcpServerNames[] = $env['name']; // Track MCP server names
401 break;
402 }
403 }
404 }
405 }
406 }
407
408 return $body;
409 }
410 else {
411 throw new Exception( 'AI Engine: Unsupported query type.' );
412 }
413 }
414
415 protected function stream_data_handler( $json ) {
416 $content = null;
417 $type = !empty( $json['type'] ) ? $json['type'] : null;
418 if ( is_null( $type ) ) {
419 return $content;
420 }
421
422 if ( $type === 'message_start' ) {
423 $usage = $json['message']['usage'];
424 $this->streamInTokens = $usage['input_tokens'];
425 $this->inModel = $json['message']['model'];
426 $this->inId = $json['message']['id'];
427
428 // Send MCP discovery event if MCP servers are configured
429 if ( $this->currentDebugMode && $this->streamCallback ) {
430 if ( !empty( $this->mcpServerNames ) ) {
431 $serverCount = count( $this->mcpServerNames );
432
433 // Get MCP tools count
434 $mcpTools = apply_filters( 'mwai_mcp_tools', [] );
435 $toolCount = count( $mcpTools );
436
437 $event = Meow_MWAI_Event::mcp_discovery( $serverCount, $toolCount )
438 ->set_metadata( 'servers', $this->mcpServerNames );
439 call_user_func( $this->streamCallback, $event );
440 }
441 }
442 }
443 else if ( $type === 'content_block_start' ) {
444 $this->streamBlocks['content'][] = $json['content_block'];
445
446 // Send "Generating response..." when we start a text block
447 if ( $this->currentDebugMode && $this->streamCallback ) {
448 $block = $json['content_block'];
449 if ( $block['type'] === 'text' && !$this->textStarted ) {
450 $this->textStarted = true;
451 $event = Meow_MWAI_Event::generating_response();
452 call_user_func( $this->streamCallback, $event );
453 }
454 }
455 }
456 else if ( $type === 'content_block_delta' ) {
457 $index = $json['index'];
458 $block = $this->streamBlocks['content'][$index];
459 if ( $json['delta']['type'] === 'text_delta' ) {
460 $block['text'] .= $json['delta']['text'];
461 $isThinkingStart = strpos( $block['text'], '<thinking' ) === 0;
462 $isThinkingEnd = strpos( $block['text'], '</thinking>' ) === 0;
463
464 if ( $isThinkingStart ) {
465 $this->streamIsThinking = true;
466 // Send thinking start event
467 if ( $this->currentDebugMode && $this->streamCallback ) {
468 $event = Meow_MWAI_Event::thinking( 'Thinking...' );
469 call_user_func( $this->streamCallback, $event );
470 }
471 }
472 if ( $isThinkingEnd ) {
473 $this->streamIsThinking = false;
474 // Send thinking end event
475 if ( $this->currentDebugMode && $this->streamCallback ) {
476 $event = Meow_MWAI_Event::thinking( 'Thinking completed.' )
477 ->set_metadata( 'status', 'completed' );
478 call_user_func( $this->streamCallback, $event );
479 }
480 }
481 $content = $json['delta']['text'];
482 }
483 else if ( $json['delta']['type'] === 'input_json_delta' ) {
484 // Somehow, the input is set as an array, but it should be a string since it's JSON.
485 $block['input'] = is_array( $block['input'] ) ? '' : $block['input'];
486 $block['input'] .= $json['delta']['partial_json'];
487
488 // Skip sending tool arguments event - too verbose
489 // if ( $this->currentDebugMode && $this->streamCallback && isset($block['type']) && $block['type'] === 'tool_use' ) {
490 // $event = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['TOOL_ARGS'] ) )
491 // ->set_content( 'Streaming tool arguments...' )
492 // ->set_metadata( 'tool_name', $block['name'] ?? 'unknown' )
493 // ->set_metadata( 'partial_args', $json['delta']['partial_json'] );
494 // call_user_func( $this->streamCallback, $event );
495 // }
496 }
497 $this->streamBlocks['content'][$index] = $block;
498 }
499 // At the end of a block, let's look for any 'input' not yet decoded from JSON
500 else if ( $type === 'content_block_stop' ) {
501 $index = $json['index'];
502 $block = $this->streamBlocks['content'][$index];
503 if ( isset( $block['input'] ) && is_string( $block['input'] ) ) {
504 $block['input'] = json_decode( $block['input'], true );
505 }
506
507 // For tool_use blocks, ensure empty inputs are objects, not arrays
508 if ( $block['type'] === 'tool_use' && isset( $block['input'] ) ) {
509 if ( empty( $block['input'] ) || ( is_array( $block['input'] ) && count( $block['input'] ) === 0 ) ) {
510 $block['input'] = new stdClass();
511 }
512 }
513
514 $this->streamBlocks['content'][$index] = $block;
515
516 // Send event for content block completion
517 if ( $this->currentDebugMode && $this->streamCallback ) {
518 if ( $block['type'] === 'mcp_tool_use' ) {
519 // Store the tool name for later lookup when we get the result
520 $this->mcpTools[$block['id']] = $block['name'];
521
522 $event = Meow_MWAI_Event::mcp_calling( $block['name'], $block['id'], $block['input'] ?? [] )
523 ->set_metadata( 'server_name', $block['server_name'] ?? 'unknown' );
524 call_user_func( $this->streamCallback, $event );
525 }
526 else if ( $block['type'] === 'mcp_tool_result' ) {
527 // Look up the tool name from the tool_use_id
528 $tool_use_id = $block['tool_use_id'] ?? '';
529 $tool_name = isset( $this->mcpTools[$tool_use_id] ) ? $this->mcpTools[$tool_use_id] : 'unknown';
530
531 $event = Meow_MWAI_Event::mcp_result( $tool_name, $tool_use_id )
532 ->set_metadata( 'content', $block['content'] ?? '' );
533 call_user_func( $this->streamCallback, $event );
534 }
535 else if ( $block['type'] === 'tool_use' ) {
536 // Regular tool use (non-MCP)
537 $event = Meow_MWAI_Event::function_calling( $block['name'] ?? 'unknown', $block['input'] ?? [] )
538 ->set_metadata( 'tool_id', $block['id'] ?? '' );
539 call_user_func( $this->streamCallback, $event );
540 }
541 else if ( $block['type'] === 'text' ) {
542 // Don't send any event here - the text generation is handled by content deltas
543 // and completion is handled by message_stop
544 }
545 else if ( $block['type'] === 'ping' ) {
546 // https://docs.anthropic.com/en/docs/build-with-claude/streaming#ping-events
547 }
548 else {
549 Meow_MWAI_Logging::log( 'Anthropic: Unknown block type in content_block_stop: ' . $block['type'] );
550 }
551 }
552 }
553 else if ( $type === 'message_delta' ) {
554 $usage = $json['usage'];
555 $this->streamOutTokens = $usage['output_tokens'];
556 }
557 else if ( $type === 'error' ) {
558 $error = $json['error'];
559 $message = $error['message'];
560
561 // Send error event
562 if ( $this->currentDebugMode && $this->streamCallback ) {
563 $event = Meow_MWAI_Event::error( $message )
564 ->set_metadata( 'error_type', $error['type'] ?? 'unknown' );
565 call_user_func( $this->streamCallback, $event );
566 }
567
568 throw new Exception( $message );
569 }
570 else if ( $type === 'message_stop' ) {
571 // Skip sending completion event - too verbose
572 // if ( $this->currentDebugMode && $this->streamCallback ) {
573 // $event = Meow_MWAI_Event::stream_completed()
574 // ->set_metadata( 'total_tokens', ($this->streamInTokens ?? 0) + ($this->streamOutTokens ?? 0) );
575 // call_user_func( $this->streamCallback, $event );
576 // }
577 }
578 else {
579 Meow_MWAI_Logging::log( "Anthropic: Unknown stream data type: $type" );
580 }
581
582 // Avoid some endings
583 $endings = [ '<|im_end|>', '</s>' ];
584 if ( in_array( $content, $endings ) ) {
585 $content = null;
586 }
587
588 // If the stream is thinking, we don't want to return anything yet.
589 if ( $this->streamIsThinking ) {
590 $content = null;
591 }
592
593 return ( $content === '0' || !empty( $content ) ) ? $content : null;
594 }
595
596 // This create the "choices" (even though, often, it is only one choice).
597 // It is basically the reply, but one that is understood by the Meow_MWAI_Reply class.
598 public function create_choices( $data ) {
599 $returned_choices = [];
600 $tool_calls = [];
601 $text_content = '';
602
603 // First, collect all tool calls and text content
604 foreach ( $data['content'] as $content ) {
605 if ( $content['type'] === 'tool_use' ) {
606 // Collect all tool calls
607 $arguments = $content['input'] ?? new stdClass();
608
609 // Ensure arguments is properly formatted
610 if ( empty( $arguments ) ) {
611 $arguments = new stdClass();
612 } else if ( is_array( $arguments ) && count( $arguments ) === 0 ) {
613 $arguments = new stdClass();
614 }
615
616 $tool_calls[] = [
617 'id' => $content['id'],
618 'type' => 'function',
619 'function' => [
620 'name' => $content['name'],
621 'arguments' => $arguments,
622 ]
623 ];
624 }
625 else if ( $content['type'] === 'text' ) {
626 $text_content .= $content['text'];
627 }
628 }
629
630 // Create a single choice with both tool calls and text content (like OpenAI does)
631 $message = [];
632
633 if ( !empty( $text_content ) ) {
634 $message['content'] = $text_content;
635 }
636
637 if ( !empty( $tool_calls ) ) {
638 $message['tool_calls'] = $tool_calls;
639 }
640
641 // Only create a choice if there's content or tool calls
642 if ( !empty( $message ) ) {
643 $returned_choices[] = [
644 'message' => $message
645 ];
646 }
647
648 return $returned_choices;
649 }
650
651 /**
652 * Override reset to include Anthropic-specific state
653 */
654 protected function reset_request_state() {
655 parent::reset_request_state();
656
657 // Reset Anthropic-specific state
658 $this->mcpTools = [];
659 $this->mcpToolCount = 0;
660 // Note: mcpServerNames is configuration, not request state
661 }
662
663 public function run_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
664 // Reset request-specific state to prevent leakage between requests
665 $this->reset_request_state();
666
667 $isStreaming = !is_null( $streamCallback );
668
669 // Initialize debug mode
670 $this->init_debug_mode( $query );
671
672 if ( $isStreaming ) {
673 $this->streamCallback = $streamCallback;
674 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
675 }
676
677 $this->reset_stream();
678 $data = null;
679 $body = $this->build_body( $query, $streamCallback );
680 $url = $this->build_url( $query );
681 $headers = $this->build_headers( $query );
682 $options = $this->build_options( $headers, $body );
683
684 // Emit "Request sent" event for feedback queries
685 if ( $this->currentDebugMode && !empty( $streamCallback ) &&
686 ( $query instanceof Meow_MWAI_Query_Feedback || $query instanceof Meow_MWAI_Query_AssistFeedback ) ) {
687 $event = Meow_MWAI_Event::request_sent()
688 ->set_metadata( 'is_feedback', true )
689 ->set_metadata( 'feedback_count', count( $query->blocks ) );
690 call_user_func( $streamCallback, $event );
691 }
692
693 try {
694 $res = $this->run_query( $url, $options, $streamCallback );
695 $reply = new Meow_MWAI_Reply( $query );
696 $returned_id = null;
697 $returned_model = null;
698 $returned_choices = [];
699
700 // Streaming Mode
701 if ( $isStreaming ) {
702 $returned_id = $this->inId;
703 $returned_model = $this->inModel ? $this->inModel : $query->model;
704 if ( !is_null( $this->streamInTokens && !is_null( $this->streamOutTokens ) ) ) {
705 $returned_in_tokens = $this->streamInTokens;
706 $returned_out_tokens = $this->streamOutTokens;
707 }
708 $data = $this->streamBlocks;
709
710 // Clean up streaming data as well
711 if ( isset( $data['content'] ) && is_array( $data['content'] ) ) {
712 foreach ( $data['content'] as &$content ) {
713 if ( $content['type'] === 'tool_use' && isset( $content['input'] ) ) {
714 if ( empty( $content['input'] ) || ( is_array( $content['input'] ) && count( $content['input'] ) === 0 ) ) {
715 $content['input'] = new stdClass();
716 }
717 }
718 }
719 unset( $content );
720 }
721
722 $returned_choices = $this->create_choices( $this->streamBlocks );
723 }
724 // Standard Mode
725 else {
726 $data = $res['data'];
727
728 // Clean up tool_use inputs in the raw data BEFORE it gets stored
729 if ( isset( $data['content'] ) && is_array( $data['content'] ) ) {
730 foreach ( $data['content'] as &$content ) {
731 if ( $content['type'] === 'tool_use' && isset( $content['input'] ) ) {
732 if ( empty( $content['input'] ) || ( is_array( $content['input'] ) && count( $content['input'] ) === 0 ) ) {
733 $content['input'] = new stdClass();
734 }
735 }
736 }
737 unset( $content );
738 }
739
740 $returned_id = $data['id'];
741 $returned_model = $data['model'];
742 $usage = $data['usage'];
743 if ( !empty( $usage ) ) {
744 $returned_in_tokens = isset( $usage['input_tokens'] ) ? $usage['input_tokens'] : null;
745 $returned_out_tokens = isset( $usage['output_tokens'] ) ? $usage['output_tokens'] : null;
746 }
747 $returned_choices = $this->create_choices( $data );
748 }
749
750
751 $reply->set_choices( $returned_choices, $data );
752 if ( !empty( $returned_id ) ) {
753 $reply->set_id( $returned_id );
754 }
755
756 // Handle tokens.
757 $this->handle_tokens_usage(
758 $reply,
759 $query,
760 $returned_model,
761 $returned_in_tokens,
762 $returned_out_tokens
763 );
764
765 return $reply;
766 }
767 catch ( Exception $e ) {
768 $error = $e->getMessage();
769 $json = json_decode( $error, true );
770 if ( json_last_error() === JSON_ERROR_NONE ) {
771 if ( isset( $json['error'] ) && isset( $json['error']['message'] ) ) {
772 $error = $json['error']['message'];
773 }
774 }
775 Meow_MWAI_Logging::error( '(Anthropic) ' . $error );
776 $service = $this->get_service_name();
777 $message = "From $service: " . $error;
778 throw new Exception( $message );
779 }
780 finally {
781 if ( $isStreaming ) {
782 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
783 }
784 }
785 }
786
787 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
788 $body = null;
789 if ( !empty( $forms ) ) {
790 $boundary = wp_generate_password( 24, false );
791 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
792 $body = $this->build_form_body( $forms, $boundary );
793 }
794 else if ( !empty( $json ) ) {
795 // For Anthropic, we need to ensure empty objects stay as objects, not arrays
796 // JSON_FORCE_OBJECT would force everything to be an object, which we don't want
797 // Instead, we've already converted empty arrays to stdClass in build_body
798 $body = json_encode( $json );
799
800 // Debug logging to verify JSON encoding
801 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
802 // Check if the body contains tool_use blocks with empty inputs
803 if ( strpos( $body, '"tool_use"' ) !== false ) {
804 error_log( 'AI Engine: Anthropic JSON body after encoding (first 1000 chars): ' . substr( $body, 0, 1000 ) );
805
806 // Check specifically for "input":[] which would be wrong
807 if ( strpos( $body, '"input":[]' ) !== false ) {
808 error_log( 'AI Engine: WARNING - Found "input":[] in JSON body, this should be "input":{} for Anthropic API' );
809 }
810 }
811 }
812 }
813 $options = [
814 'headers' => $headers,
815 'method' => $method,
816 'timeout' => MWAI_TIMEOUT,
817 'body' => $body,
818 'sslverify' => false
819 ];
820 return $options;
821 }
822
823 protected function get_service_name() {
824 return 'Anthropic';
825 }
826
827 public function get_models() {
828 return apply_filters( 'mwai_anthropic_models', MWAI_ANTHROPIC_MODELS );
829 }
830
831 public static function get_models_static() {
832 return MWAI_ANTHROPIC_MODELS;
833 }
834
835 public function handle_tokens_usage(
836 $reply,
837 $query,
838 $returned_model,
839 $returned_in_tokens,
840 $returned_out_tokens,
841 $returned_price = null
842 ) {
843 $returned_in_tokens = !is_null( $returned_in_tokens ) ?
844 $returned_in_tokens : $reply->get_in_tokens( $query );
845 $returned_out_tokens = !is_null( $returned_out_tokens ) ?
846 $returned_out_tokens : $reply->get_out_tokens();
847 if ( !empty( $reply->id ) ) {
848 // Would be cool to retrieve the usage from the API, but it's not possible.
849 }
850 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
851 $reply->set_usage( $usage );
852
853 // Set accuracy based on data availability
854 if ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
855 // Anthropic provides token counts from API = tokens accuracy
856 $reply->set_usage_accuracy( 'tokens' );
857 } else {
858 // Fallback to estimated
859 $reply->set_usage_accuracy( 'estimated' );
860 }
861 }
862
863 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
864 return parent::get_price( $query, $reply );
865 }
866
867 /**
868 * Check the connection to Anthropic by listing available models.
869 * Anthropic doesn't provide a models endpoint, so we just verify authentication works.
870 */
871 public function connection_check() {
872 try {
873 // Get the endpoint
874 $endpoint = apply_filters( 'mwai_anthropic_endpoint', 'https://api.anthropic.com/v1', $this->env );
875
876 // For Anthropic, we'll use the messages endpoint with a minimal request to verify auth
877 $url = trailingslashit( $endpoint ) . 'messages';
878
879 // Create a minimal query just to test authentication
880 $testBody = [
881 'model' => 'claude-3-haiku-20240307', // Use cheapest model
882 'max_tokens' => 1,
883 'messages' => [
884 ['role' => 'user', 'content' => 'Hi']
885 ],
886 'metadata' => [
887 'user_id' => 'connection_test'
888 ]
889 ];
890
891 // Build headers with a dummy query
892 $dummyQuery = new Meow_MWAI_Query_Text( 'test' );
893 $headers = $this->build_headers( $dummyQuery );
894 $options = $this->build_options( $headers, $testBody );
895
896 // Try to make a minimal request
897 $response = $this->run_query( $url, $options );
898
899 // If we get here without exception, the API key is valid
900 // Get the list of available models from our constants
901 $models = $this->get_models();
902 $modelNames = array_map( function( $model ) {
903 return $model['model'] ?? $model['name'] ?? 'unknown';
904 }, $models );
905
906 return [
907 'models' => array_slice( $modelNames, 0, 10 ), // Return first 10 models
908 'service' => 'Anthropic'
909 ];
910 }
911 catch ( Exception $e ) {
912 // Check if it's an authentication error
913 $message = $e->getMessage();
914 if ( strpos( $message, 'authentication_error' ) !== false ||
915 strpos( $message, 'invalid x-api-key' ) !== false ||
916 strpos( $message, '401' ) !== false ) {
917 throw new Exception( 'Invalid API key' );
918 }
919 throw new Exception( 'Connection failed: ' . $message );
920 }
921 }
922 }
923