PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.5
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.5
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 / core.php
ai-engine / classes / engines Last commit date
traits 1 year ago anthropic.php 1 year ago chatml.php 1 year ago core.php 1 year ago factory.php 1 year ago google.php 1 year ago hugging-face.php 1 year ago open-router.php 1 year ago openai.php 1 year ago perplexity.php 1 year ago replicate.php 1 year ago
core.php
587 lines
1 <?php
2
3 class Meow_MWAI_Engines_Core {
4 protected $core = null;
5 public $env = null;
6 public $envId = null;
7 public $envType = null;
8
9 // Streaming
10 protected $streamCallback = null;
11 protected $streamTemporaryBuffer = '';
12 protected $streamBuffer = '';
13 protected $streamHeaders = [];
14 protected $streamContent = '';
15
16 // Debug mode for stream events
17 protected $currentDebugMode = false;
18 protected $currentQuery = null;
19 protected $emittedFunctionResults = [];
20
21 public function __construct( $core, $env ) {
22 $this->core = $core;
23 $this->env = $env;
24 $this->envId = isset( $env['id'] ) ? $env['id'] : null;
25 $this->envType = isset( $env['type'] ) ? $env['type'] : null;
26 }
27
28 /**
29 * Reset all request-specific state variables.
30 * This should be called at the start of each new request to prevent
31 * state leakage between requests.
32 */
33 protected function reset_request_state() {
34 // Reset streaming state
35 $this->streamCallback = null;
36 $this->streamTemporaryBuffer = '';
37 $this->streamBuffer = '';
38 $this->streamHeaders = [];
39 $this->streamContent = '';
40
41 // Reset debug/event state
42 $this->currentDebugMode = false;
43 $this->currentQuery = null;
44 $this->emittedFunctionResults = [];
45 }
46
47 public function run( $query, $streamCallback = null, $maxDepth = 5 ) {
48
49 // Apply filter to allow overriding maxDepth (only on first call)
50 if ( !isset( $query->_maxDepthConfigured ) ) {
51 $maxDepth = apply_filters( 'mwai_function_call_max_depth', $maxDepth, $query );
52 $query->_maxDepthConfigured = $maxDepth;
53 }
54
55 // Check if queries debug is enabled
56 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
57
58 // Log query start if debug is enabled
59 if ( $queries_debug ) {
60 // We'll let the individual engines log the actual HTTP requests/responses
61 // Just log a simple start marker here
62 error_log( '[AI Engine Queries Debug] ========================================' );
63 $query_type = get_class( $query );
64 error_log( '[AI Engine Queries Debug] Starting ' . $query_type . ' to ' . ( $query->model ?? 'unknown model' ) );
65 }
66
67 // Check if the query is allowed.
68 $limits = $this->core->get_option( 'limits' );
69 $allowed = apply_filters( 'mwai_ai_allowed', true, $query, $limits );
70 if ( $allowed !== true ) {
71 $message = is_string( $allowed ) ? $allowed : 'Unauthorized query.';
72 throw new Exception( $message );
73 }
74
75 // Important as it makes sure everything is consolidated in the query and the engine.
76 $this->final_checks( $query );
77
78 // Run the query
79 $reply = null;
80 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
81 $reply = $this->run_completion_query( $query, $streamCallback );
82 }
83 else if ( $query instanceof Meow_MWAI_Query_Assistant || $query instanceof Meow_MWAI_Query_AssistFeedback ) {
84 $reply = $this->run_assistant_query( $query, $streamCallback );
85 if ( $reply === null ) {
86 throw new Exception( 'Assistants are not supported in this version of AI Engine.' );
87 }
88 }
89 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
90 $reply = $this->run_embedding_query( $query );
91 }
92 else if ( $query instanceof Meow_MWAI_Query_EditImage ) {
93 $reply = $this->run_editimage_query( $query );
94 }
95 else if ( $query instanceof Meow_MWAI_Query_Image ) {
96 $reply = $this->run_image_query( $query );
97 }
98 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
99 $reply = $this->run_transcribe_query( $query );
100 }
101 else {
102 throw new Exception( 'Unknown query type.' );
103 }
104
105 // Allow to modify the reply before it is sent.
106 $reply = apply_filters( 'mwai_ai_reply', $reply, $query );
107
108 // Log query completion if debug is enabled
109 if ( $queries_debug && empty( $reply->needFeedbacks ) ) {
110 error_log( '[AI Engine Queries Debug] Query completed' );
111 error_log( '[AI Engine Queries Debug] ========================================' );
112 }
113
114 // Function Call Handling - This is where the magic happens!
115 // When the AI model requests function calls, we execute them and send results back
116 if ( !empty( $reply->needFeedbacks ) ) {
117
118 // Debug: Log how many needFeedbacks we have
119 if ( $queries_debug ) {
120 error_log( '[AI Engine Queries Debug] Core: Processing ' . count( $reply->needFeedbacks ) . ' needFeedbacks' );
121 foreach ( $reply->needFeedbacks as $idx => $feedback ) {
122 error_log( '[AI Engine Queries Debug] Core: needFeedback[' . $idx . ']: name=' . $feedback['name'] . ', toolId=' . ( $feedback['toolId'] ?? 'none' ) );
123 }
124 }
125
126
127 // Prevent infinite loops - each function call reduces maxDepth by 1
128 if ( $maxDepth <= 0 ) {
129 // Build call stack for better debugging
130 $callStack = [];
131 foreach ( $reply->needFeedbacks as $feedback ) {
132 $callStack[] = $feedback['name'] ?? 'unknown';
133 }
134
135 throw Meow_MWAI_FunctionCallException::loop_detected(
136 $query->_maxDepthConfigured ?? 5, // Use configured max depth
137 $callStack
138 );
139 }
140
141 // Create a feedback query if we're not already in one
142 // This wraps the original query with function execution results
143 if ( !( $query instanceof Meow_MWAI_Query_AssistFeedback ) && !( $query instanceof Meow_MWAI_Query_Feedback ) ) {
144 $queryClass = $query instanceof Meow_MWAI_Query_Assistant ?
145 Meow_MWAI_Query_AssistFeedback::class : Meow_MWAI_Query_Feedback::class;
146 // Note: $reply->query contains the original query that produced this reply
147 $query = new $queryClass( $reply, $reply->query );
148 }
149
150 // Validate that all function calls have proper function definitions
151 foreach ( $reply->needFeedbacks as $needFeedback ) {
152 if ( !isset( $needFeedback['function'] ) ) {
153 $functionName = $needFeedback['name'] ?? 'unknown';
154 $availableFunctions = array_map( function( $f ) { return $f->name; }, $query->functions );
155
156 throw new Exception( sprintf(
157 "Function '%s' not found in query functions. Available functions: %s",
158 $functionName,
159 implode( ', ', $availableFunctions )
160 ) );
161 }
162 }
163
164 // Group function calls by their source message to maintain proper context
165 // This ensures related function calls are processed together
166 $feedback_blocks = [];
167 foreach ( $reply->needFeedbacks as $needFeedback ) {
168 $rawMessageKey = md5( serialize( $needFeedback['rawMessage'] ) );
169
170 // Initialize the feedback block for this rawMessage if it hasn't been initialized yet
171 if ( !isset( $feedback_blocks[$rawMessageKey] ) ) {
172 $feedback_blocks[$rawMessageKey] = [
173 'rawMessage' => $needFeedback['rawMessage'],
174 'feedbacks' => []
175 ];
176 }
177
178 // Get the value related to this feedback (usually, a function call)
179 $value = apply_filters( 'mwai_ai_feedback', null, $needFeedback, $reply );
180
181 if ( $value === null ) {
182 // Check if the function handler exists
183 if ( !has_filter( 'mwai_ai_feedback' ) ) {
184 Meow_MWAI_Logging::error(
185 Meow_MWAI_FunctionCallException::missing_function_handler(
186 $needFeedback['name']
187 )->getMessage()
188 );
189 }
190 else {
191 Meow_MWAI_Logging::warn( "The returned value for '{$needFeedback['name']}' was null." );
192 }
193 $value = '[NO VALUE RETURNED - DO NOT SHOW THIS]';
194 }
195
196 // Emit "Got result" event and log for debugging
197 if ( $this->currentDebugMode ) {
198 // Format the result preview
199 $resultPreview = is_array( $value ) ? json_encode( $value ) : (string) $value;
200 if ( strlen( $resultPreview ) > 100 ) {
201 $resultPreview = substr( $resultPreview, 0, 100 ) . '...';
202 }
203
204 // Log the function result for debugging
205 Meow_MWAI_Logging::log( "Function '{$needFeedback['name']}' returned: " . $resultPreview );
206
207 // Emit function result event if we have a callback
208 if ( !empty( $streamCallback ) ) {
209 // Load event helper if not already loaded
210 if ( !class_exists( 'Meow_MWAI_Event' ) ) {
211 require_once MWAI_PATH . '/classes/event.php';
212 }
213
214 $functionName = $needFeedback['name'];
215
216 $event = Meow_MWAI_Event::function_result( $functionName )
217 ->set_metadata( 'result', $resultPreview )
218 ->set_metadata( 'tool_id', $needFeedback['toolId'] ?? null );
219 call_user_func( $streamCallback, $event );
220 }
221 }
222
223 // Add the feedback information to the appropriate feedback block
224 $feedback_blocks[$rawMessageKey]['feedbacks'][] = [
225 'request' => $needFeedback, // TODO: Meow_MWAI_Feedback_Request
226 'reply' => [ 'value' => $value ] // TODO: Meow_MWAI_Feedback_Reply
227 ];
228 }
229
230 $query->clear_feedback_blocks();
231 foreach ( $feedback_blocks as $feedback_block ) {
232 $query->add_feedback_block( $feedback_block );
233 }
234
235 // Log feedback query if debug is enabled
236 if ( $queries_debug ) {
237 error_log( '[AI Engine Queries Debug] Processing feedback query with ' . count( $feedback_blocks ) . ' feedback blocks' );
238 }
239
240 // Run the feedback query
241 $reply = $this->run( $query, $streamCallback, $maxDepth - 1 );
242 }
243
244 return $reply;
245 }
246
247 public function retrieve_model_info( $model ) {
248 $models = $this->get_models();
249 foreach ( $models as $currentModel ) {
250 if ( $currentModel['model'] === $model ) {
251 return $currentModel;
252 }
253 }
254 return false;
255 }
256
257 public function final_checks( Meow_MWAI_Query_Base $query ) {
258 $query->final_checks();
259 //$found = false;
260
261 // Check if the model is available, except if it's an assistant
262 if ( !( $query instanceof Meow_MWAI_Query_Assistant ) ) {
263 // TODO: Avoid checking on the finetuned models for now.
264 if ( substr( $query->model, 0, 3 ) === 'ft:' ) {
265 return;
266 }
267 $model_info = $this->retrieve_model_info( $query->model );
268 if ( $model_info === false ) {
269 throw new Exception( "AI Engine: The model '{$query->model}' is not available." );
270 }
271 if ( isset( $model_info['mode'] ) ) {
272 $query->mode = $model_info['mode'];
273 }
274 }
275 }
276
277 // Streamline the messages:
278 // - Concatenate consecutive model messages into a single message for the model role
279 // - Make sure the first message is a user message
280 // - Make sure the last message is a user message
281 protected function streamline_messages( $messages, $systemRole = 'assistant', $messageType = 'content' ) {
282 $processedMessages = [];
283 $lastRole = '';
284 $concatenatedText = '';
285
286 // Determine the way to access message content based on messageType
287 $getContent = function ( $message ) use ( $messageType ) {
288 if ( $messageType == 'parts' ) {
289 return $message['parts'][0]['text'];
290 }
291 else { // Default to 'content'
292 return $message['content'];
293 }
294 };
295
296 // Set content to a message depending on the messageType
297 $setContent = function ( &$message, $content ) use ( $messageType ) {
298 if ( $messageType == 'parts' ) {
299 $message['parts'] = [['text' => $content]];
300 }
301 else { // Default to 'content'
302 $message['content'] = $content;
303 }
304 };
305
306 // Concatenate consecutive model messages into a single message for the model role
307 foreach ( $messages as $message ) {
308 if ( $message['role'] == $systemRole ) {
309 if ( $lastRole == $systemRole ) {
310 $concatenatedText .= "\n" . $getContent( $message );
311 }
312 else {
313 if ( $concatenatedText !== '' ) {
314 $newMessage = [ 'role' => $systemRole ];
315 $setContent( $newMessage, $concatenatedText );
316 $processedMessages[] = $newMessage;
317 }
318 $concatenatedText = $getContent( $message );
319 }
320 }
321 else {
322 if ( $lastRole == $systemRole ) {
323 $newMessage = [ 'role' => $systemRole ];
324 $setContent( $newMessage, $concatenatedText );
325 $processedMessages[] = $newMessage;
326 $concatenatedText = '';
327 }
328 $processedMessages[] = $message;
329 }
330 $lastRole = $message['role'];
331 }
332 if ( $lastRole == $systemRole && $concatenatedText !== '' ) {
333 $newMessage = [ 'role' => $systemRole ];
334 $setContent( $newMessage, $concatenatedText );
335 $processedMessages[] = $newMessage;
336 }
337
338 // Make sure the last message is a user message, if not, throw an exception
339 if ( end( $processedMessages )['role'] !== 'user' ) {
340 throw new Exception( 'The last message must be a user message.' );
341 }
342
343 // Make sure the first message is a user message, if not, add an empty user message
344 if ( $processedMessages[0]['role'] !== 'user' ) {
345 $newMessage = [ 'role' => 'user' ];
346 $setContent( $newMessage, '' );
347 array_unshift( $processedMessages, $newMessage );
348 }
349
350 return $processedMessages;
351 }
352
353 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
354 public function stream_error_check( $data ) {
355 if ( strpos( $data, 'error' ) === false ) {
356 return;
357 }
358
359 $data = trim( $data );
360 $jsonPart = $data;
361 if ( strpos( $jsonPart, 'data:' ) === 0 ) {
362 $jsonPart = trim( substr( $jsonPart, strlen( 'data:' ) ) );
363 }
364
365 $json = json_decode( $jsonPart, true );
366 if ( json_last_error() !== JSON_ERROR_NONE ) {
367 return; // not valid JSON, nothing to do
368 }
369 // 1. OpenAI style: { error: {...} }
370 $error = null;
371 if ( isset( $json['error'] ) ) {
372 $error = $json['error'];
373 }
374 // 2. Google style: [ { error: {...} } ]
375 else if ( is_array( $json ) ) {
376 foreach ( $json as $item ) {
377 if ( isset( $item['error'] ) ) {
378 $error = $item['error'];
379 break;
380 }
381 }
382 }
383 // 3. Some APIs return { type: "error", message: ... }
384 else if ( isset( $json['type'] ) && $json['type'] === 'error' ) {
385 $error = $json;
386 }
387
388 if ( is_null( $error ) ) {
389 return;
390 }
391
392 $message = $error['message'] ?? ( is_string( $error ) ? $error : null );
393 $code = $error['code'] ?? null;
394 // Google uses "status" instead of "type" – accept both
395 $type = $error['type'] ?? ( $error['status'] ?? null );
396 if ( is_null( $message ) ) {
397 throw new Exception( 'Unknown error (stream_error_check).' );
398 }
399
400 $errorMessage = "Error: $message";
401 if ( !is_null( $code ) ) {
402 $errorMessage .= " ($code)";
403 }
404 if ( !is_null( $type ) ) {
405 $errorMessage .= " ($type)";
406 }
407
408 throw new Exception( $errorMessage );
409 }
410
411 protected function init_debug_mode( $query ) {
412 // Check if debug mode is enabled in settings
413 $this->currentDebugMode = $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'debug_mode' );
414 $this->currentQuery = $query;
415 }
416
417 public function stream_handler( $handle, $args, $url ) {
418 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
419 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
420
421 // TODO: This is breaking the response. We need to find a way to handle the headers.
422 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function ( $curl, $header ) {
423 // $length = strlen( $header );
424 // $this->streamHeaders[] = $header;
425 // $this->stream_header_handler( $header );
426 // return $length;
427 // });
428
429 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) use ( $url ) {
430 $length = strlen( $data );
431
432 // Log streaming data if queries debug is enabled
433 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
434 static $logged_url = false;
435 if ( $queries_debug && !$logged_url ) {
436 error_log( '[AI Engine Queries Debug] Streaming from: ' . $url );
437 $logged_url = true;
438 }
439
440 // Bufferize the unfinished stream (if it's the case)
441 $this->streamTemporaryBuffer .= $data;
442 $this->streamBuffer .= $data;
443
444 // Error Management
445 $this->stream_error_check( $this->streamBuffer );
446
447 $lines = explode( "\n", $this->streamTemporaryBuffer );
448 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
449 $this->streamTemporaryBuffer = array_pop( $lines );
450 }
451 else {
452 $this->streamTemporaryBuffer = '';
453 }
454
455 foreach ( $lines as $line ) {
456 if ( $line === '' ) {
457 continue;
458 }
459 if ( strpos( $line, 'data:' ) === 0 ) {
460 $line = trim( substr( $line, 5 ) );
461 $json = json_decode( trim( $line ), true );
462
463 if ( json_last_error() === JSON_ERROR_NONE ) {
464 // Log individual streaming event if queries debug is enabled
465 static $event_count = 0;
466 if ( $queries_debug && $event_count < 10 ) {
467 // Log only the event type and key data, not the entire response
468 $event_log = [
469 'type' => $json['type'] ?? 'unknown'
470 ];
471
472 // Add specific details based on event type
473 if ( isset( $json['type'] ) ) {
474 if ( $json['type'] === 'response.output_item.added' && isset( $json['item'] ) ) {
475 $event_log['item_type'] = $json['item']['type'] ?? 'unknown';
476 $event_log['name'] = $json['item']['name'] ?? null;
477 $event_log['call_id'] = $json['item']['call_id'] ?? null;
478 }
479 elseif ( strpos( $json['type'], 'response.function_call' ) === 0 ) {
480 $event_log['call_id'] = $json['call_id'] ?? $json['item_id'] ?? null;
481 }
482 elseif ( $json['type'] === 'response.output_item.done' && isset( $json['item'] ) ) {
483 $event_log['item_type'] = $json['item']['type'] ?? 'unknown';
484 if ( isset( $json['item']['call_id'] ) ) {
485 $event_log['call_id'] = $json['item']['call_id'];
486 }
487 }
488 }
489
490 error_log( '[AI Engine Queries Debug] Event: ' . json_encode( $event_log ) );
491 $event_count++;
492 }
493
494 $content = $this->stream_data_handler( $json );
495 if ( !is_null( $content ) ) {
496
497 // Check if content is an Event object
498 if ( is_object( $content ) && $content instanceof Meow_MWAI_Event ) {
499 // For Event objects, pass the object directly to callback
500 // Don't accumulate in streamContent as it's not regular text
501 call_user_func( $this->streamCallback, $content );
502 }
503 else {
504 // For regular string content
505
506 // TO CHECK: Not sure why we need to do this to make sure there is a line return in the chatbot
507 // If we don't do this, HuggingFace streams "\n" as a token without anything else, and the
508 // chatbot doesn't display it.
509 if ( $content === "\n" ) {
510 $content = " \n";
511 }
512
513 $this->streamContent .= $content;
514 call_user_func( $this->streamCallback, $content );
515 }
516 }
517 }
518 else if ( $line !== '[DONE]' && !empty( $line ) ) {
519 $this->streamTemporaryBuffer .= $line . "\n";
520 }
521 }
522 }
523 return $length;
524 } );
525 }
526
527 protected function stream_header_handler( $header ) {
528
529 }
530
531 protected function stream_data_handler( $json ) {
532 throw new Exception( 'Not implemented.' );
533 }
534
535 public function get_models() {
536 throw new Exception( 'Not implemented.' );
537 }
538
539 public function retrieve_models() {
540 throw new Exception( 'Not implemented.' );
541 }
542
543 public function run_completion_query( Meow_MWAI_Query_Base $query, $streamCallback = null ): Meow_MWAI_Reply {
544 throw new Exception( 'Not implemented.' );
545 }
546
547 public function run_assistant_query( Meow_MWAI_Query_Assistant $query, $streamCallback = null ): Meow_MWAI_Reply {
548 throw new Exception( 'Not implemented, or not supported in this version of AI Engine.' );
549 }
550
551 public function run_embedding_query( Meow_MWAI_Query_Base $query ) {
552 throw new Exception( 'Not implemented.' );
553 }
554
555 public function run_image_query( Meow_MWAI_Query_Base $query ) {
556 throw new Exception( 'Not implemented.' );
557 }
558
559 public function run_editimage_query( Meow_MWAI_Query_Base $query ) {
560 throw new Exception( 'Not implemented.' );
561 }
562
563 public function run_transcribe_query( Meow_MWAI_Query_Base $query ) {
564 throw new Exception( 'Not implemented.' );
565 }
566
567 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
568 throw new Exception( 'Not implemented.' );
569 }
570
571 /**
572 * Check the connection to the AI service.
573 * This should be a minimal, cost-free API call to verify credentials and connectivity.
574 *
575 * @return array {
576 * @type bool $success Whether the connection test was successful
577 * @type string $service The service name (e.g., 'OpenAI', 'Anthropic')
578 * @type string $message A human-readable message about the test result
579 * @type array $details Additional service-specific details
580 * @type string $error Error message if the test failed
581 * }
582 */
583 public function connection_check() {
584 throw new Exception( 'Connection check not implemented for this service.' );
585 }
586 }
587