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