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 / core.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
core.php
651 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] ========================================' );
63 $query_type = get_class( $query );
64 error_log( '[AI Engine Queries] 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 // For embedding queries, just log the dimensions count
111 if ( $query instanceof Meow_MWAI_Query_Embed && !empty( $reply->result ) && is_array( $reply->result ) ) {
112 error_log( '[AI Engine Queries] Embedding completed with ' . count( $reply->result ) . ' dimensions' );
113 }
114 else {
115 error_log( '[AI Engine Queries] Query completed' );
116 }
117 error_log( '[AI Engine Queries] ========================================' );
118 }
119
120 // Function Call Handling - This is where the magic happens!
121 // When the AI model requests function calls, we execute them and send results back
122 if ( !empty( $reply->needFeedbacks ) ) {
123
124 // Debug: Log how many needFeedbacks we have
125 if ( $queries_debug ) {
126 error_log( '[AI Engine Queries] Core: Processing ' . count( $reply->needFeedbacks ) . ' needFeedbacks' );
127 foreach ( $reply->needFeedbacks as $idx => $feedback ) {
128 error_log( '[AI Engine Queries] Core: needFeedback[' . $idx . ']: name=' . $feedback['name'] . ', toolId=' . ( $feedback['toolId'] ?? 'none' ) );
129 }
130 }
131
132
133
134 // Prevent infinite loops - each function call reduces maxDepth by 1
135 if ( $maxDepth <= 0 ) {
136 // Build call stack for better debugging
137 $callStack = [];
138 foreach ( $reply->needFeedbacks as $feedback ) {
139 $callStack[] = $feedback['name'] ?? 'unknown';
140 }
141
142 throw Meow_MWAI_FunctionCallException::loop_detected(
143 $query->_maxDepthConfigured ?? 5, // Use configured max depth
144 $callStack
145 );
146 }
147
148 // Create a feedback query if we're not already in one
149 // This wraps the original query with function execution results
150 if ( !( $query instanceof Meow_MWAI_Query_AssistFeedback ) && !( $query instanceof Meow_MWAI_Query_Feedback ) ) {
151 $queryClass = $query instanceof Meow_MWAI_Query_Assistant ?
152 Meow_MWAI_Query_AssistFeedback::class : Meow_MWAI_Query_Feedback::class;
153 // Note: $reply->query contains the original query that produced this reply
154 $query = new $queryClass( $reply, $reply->query );
155 }
156
157 // Validate that all function calls have proper function definitions
158 foreach ( $reply->needFeedbacks as $needFeedback ) {
159 if ( !isset( $needFeedback['function'] ) ) {
160 $functionName = $needFeedback['name'] ?? 'unknown';
161 $availableFunctions = array_map( function( $f ) { return $f->name; }, $query->functions );
162
163 throw new Exception( sprintf(
164 "Function '%s' not found in query functions. Available functions: %s",
165 $functionName,
166 implode( ', ', $availableFunctions )
167 ) );
168 }
169 }
170
171 // Group function calls by their source message to maintain proper context
172 // This ensures related function calls are processed together
173 $feedback_blocks = [];
174
175 // Special handling for Responses API - group all function calls together
176 // Check if we're using Responses API by looking at the query's previous response ID or reply ID
177 $isResponsesApi = false;
178
179 // Method 1: Check if query has a previous response ID from Responses API
180 if ( !empty( $query->previousResponseId ) && $this->core->responseIdManager->is_valid_for_responses_api( $query->previousResponseId ) ) {
181 $isResponsesApi = true;
182 }
183
184 // Method 2: Check if the reply has a Responses API response ID
185 if ( !$isResponsesApi && !empty( $reply->id ) && $this->core->responseIdManager->is_valid_for_responses_api( $reply->id ) ) {
186 $isResponsesApi = true;
187 }
188
189 // Method 3: Check the model tags for 'responses' tag
190 if ( !$isResponsesApi && !empty( $query->model ) ) {
191 $modelInfo = $this->retrieve_model_info( $query->model );
192 if ( $modelInfo && !empty( $modelInfo['tags'] ) && in_array( 'responses', $modelInfo['tags'] ) ) {
193 // Also check if Responses API is enabled in settings
194 $responsesApiEnabled = $this->core->get_option( 'ai_responses_api' ) ?? true;
195 if ( $responsesApiEnabled ) {
196 $isResponsesApi = true;
197 }
198 }
199 }
200
201 // Method 4: For OpenAI engine, check if we're already using Responses API
202 // This is important for models that use Responses API but don't have the tag
203 if ( !$isResponsesApi && method_exists( $this, 'should_use_responses_api' ) ) {
204 // This is an OpenAI engine, check if it should use Responses API
205 $isResponsesApi = $this->should_use_responses_api( $query->model );
206 }
207
208 // Debug: Log grouping information
209 if ( $queries_debug ) {
210 error_log( '[AI Engine Queries] Grouping ' . count( $reply->needFeedbacks ) . ' function calls' );
211 error_log( '[AI Engine Queries] Is Responses API: ' . ( $isResponsesApi ? 'yes' : 'no' ) );
212 error_log( '[AI Engine Queries] Detection methods:' );
213 error_log( '[AI Engine Queries] - previousResponseId: ' . ( $query->previousResponseId ?? 'null' ) );
214 error_log( '[AI Engine Queries] - reply->id: ' . ( $reply->id ?? 'null' ) );
215 error_log( '[AI Engine Queries] - model: ' . ( $query->model ?? 'null' ) );
216 error_log( '[AI Engine Queries] - method_exists should_use_responses_api: ' . ( method_exists( $this, 'should_use_responses_api' ) ? 'yes' : 'no' ) );
217 error_log( '[AI Engine Queries] - engine class: ' . get_class( $this ) );
218 if ( $isResponsesApi ) {
219 error_log( '[AI Engine Queries] All function calls will be grouped together for Responses API' );
220 }
221 }
222
223 foreach ( $reply->needFeedbacks as $idx => $needFeedback ) {
224 // For Responses API, use a single key to group all function calls together
225 $rawMessageKey = md5( serialize( $needFeedback['rawMessage'] ) );
226
227 if ( $queries_debug ) {
228 error_log( '[AI Engine Queries] Function call ' . $idx . ': ' . $needFeedback['name'] . ' (key: ' . substr( $rawMessageKey, 0, 8 ) . ')' );
229 }
230
231 // Initialize the feedback block for this rawMessage if it hasn't been initialized yet
232 if ( !isset( $feedback_blocks[$rawMessageKey] ) ) {
233 $feedback_blocks[$rawMessageKey] = [
234 'rawMessage' => $needFeedback['rawMessage'],
235 'feedbacks' => []
236 ];
237 }
238
239 // Get the value related to this feedback (usually, a function call)
240 $value = apply_filters( 'mwai_ai_feedback', null, $needFeedback, $reply );
241
242 if ( $value === null ) {
243 // Check if the function handler exists
244 if ( !has_filter( 'mwai_ai_feedback' ) ) {
245 Meow_MWAI_Logging::error(
246 Meow_MWAI_FunctionCallException::missing_function_handler(
247 $needFeedback['name']
248 )->getMessage()
249 );
250 }
251 else {
252 Meow_MWAI_Logging::warn( "The returned value for '{$needFeedback['name']}' was null." );
253 }
254 $value = '[NO VALUE RETURNED - DO NOT SHOW THIS]';
255 }
256
257 // Emit "Got result" event and log for debugging
258 if ( $this->currentDebugMode ) {
259 // Format the result preview
260 $resultPreview = is_array( $value ) ? json_encode( $value ) : (string) $value;
261 if ( strlen( $resultPreview ) > 100 ) {
262 $resultPreview = substr( $resultPreview, 0, 100 ) . '...';
263 }
264
265 // Log the function result for debugging
266 Meow_MWAI_Logging::log( "Function '{$needFeedback['name']}' returned: " . $resultPreview );
267
268 // Emit function result event if we have a callback
269 if ( !empty( $streamCallback ) ) {
270 // Load event helper if not already loaded
271 if ( !class_exists( 'Meow_MWAI_Event' ) ) {
272 require_once MWAI_PATH . '/classes/event.php';
273 }
274
275 $functionName = $needFeedback['name'];
276
277 $event = Meow_MWAI_Event::function_result( $functionName )
278 ->set_metadata( 'result', $resultPreview )
279 ->set_metadata( 'tool_id', $needFeedback['toolId'] ?? null );
280 call_user_func( $streamCallback, $event );
281 }
282 }
283
284 // Add the feedback information to the appropriate feedback block
285 $feedback_blocks[$rawMessageKey]['feedbacks'][] = [
286 'request' => $needFeedback, // TODO: Meow_MWAI_Feedback_Request
287 'reply' => [ 'value' => $value ] // TODO: Meow_MWAI_Feedback_Reply
288 ];
289 }
290
291 $query->clear_feedback_blocks();
292 foreach ( $feedback_blocks as $feedback_block ) {
293 $query->add_feedback_block( $feedback_block );
294 }
295
296 // Log feedback query if debug is enabled
297 if ( $queries_debug ) {
298 error_log( '[AI Engine Queries] Created ' . count( $feedback_blocks ) . ' feedback blocks from ' . count( $reply->needFeedbacks ) . ' function calls' );
299 foreach ( $feedback_blocks as $key => $block ) {
300 error_log( '[AI Engine Queries] Block ' . substr( $key, 0, 8 ) . ' has ' . count( $block['feedbacks'] ) . ' feedbacks' );
301 }
302 }
303
304 // Run the feedback query
305 $reply = $this->run( $query, $streamCallback, $maxDepth - 1 );
306 }
307
308 return $reply;
309 }
310
311 public function retrieve_model_info( $model ) {
312 $models = $this->get_models();
313 foreach ( $models as $currentModel ) {
314 if ( $currentModel['model'] === $model ) {
315 return $currentModel;
316 }
317 }
318 return false;
319 }
320
321 public function final_checks( Meow_MWAI_Query_Base $query ) {
322 $query->final_checks();
323 //$found = false;
324
325 // Check if the model is available, except if it's an assistant
326 if ( !( $query instanceof Meow_MWAI_Query_Assistant ) ) {
327 // TODO: Avoid checking on the finetuned models for now.
328 if ( substr( $query->model, 0, 3 ) === 'ft:' ) {
329 return;
330 }
331 $model_info = $this->retrieve_model_info( $query->model );
332 if ( $model_info === false ) {
333 throw new Exception( "AI Engine: The model '{$query->model}' is not available." );
334 }
335 if ( isset( $model_info['mode'] ) ) {
336 $query->mode = $model_info['mode'];
337 }
338 }
339 }
340
341 // Streamline the messages:
342 // - Concatenate consecutive model messages into a single message for the model role
343 // - Make sure the first message is a user message
344 // - Make sure the last message is a user message
345 protected function streamline_messages( $messages, $systemRole = 'assistant', $messageType = 'content' ) {
346 $processedMessages = [];
347 $lastRole = '';
348 $concatenatedText = '';
349
350 // Determine the way to access message content based on messageType
351 $getContent = function ( $message ) use ( $messageType ) {
352 if ( $messageType == 'parts' ) {
353 return $message['parts'][0]['text'];
354 }
355 else { // Default to 'content'
356 return $message['content'];
357 }
358 };
359
360 // Set content to a message depending on the messageType
361 $setContent = function ( &$message, $content ) use ( $messageType ) {
362 if ( $messageType == 'parts' ) {
363 $message['parts'] = [['text' => $content]];
364 }
365 else { // Default to 'content'
366 $message['content'] = $content;
367 }
368 };
369
370 // Concatenate consecutive model messages into a single message for the model role
371 foreach ( $messages as $message ) {
372 if ( $message['role'] == $systemRole ) {
373 if ( $lastRole == $systemRole ) {
374 $concatenatedText .= "\n" . $getContent( $message );
375 }
376 else {
377 if ( $concatenatedText !== '' ) {
378 $newMessage = [ 'role' => $systemRole ];
379 $setContent( $newMessage, $concatenatedText );
380 $processedMessages[] = $newMessage;
381 }
382 $concatenatedText = $getContent( $message );
383 }
384 }
385 else {
386 if ( $lastRole == $systemRole ) {
387 $newMessage = [ 'role' => $systemRole ];
388 $setContent( $newMessage, $concatenatedText );
389 $processedMessages[] = $newMessage;
390 $concatenatedText = '';
391 }
392 $processedMessages[] = $message;
393 }
394 $lastRole = $message['role'];
395 }
396 if ( $lastRole == $systemRole && $concatenatedText !== '' ) {
397 $newMessage = [ 'role' => $systemRole ];
398 $setContent( $newMessage, $concatenatedText );
399 $processedMessages[] = $newMessage;
400 }
401
402 // Make sure the last message is a user message, if not, throw an exception
403 if ( end( $processedMessages )['role'] !== 'user' ) {
404 throw new Exception( 'The last message must be a user message.' );
405 }
406
407 // Make sure the first message is a user message, if not, add an empty user message
408 if ( $processedMessages[0]['role'] !== 'user' ) {
409 $newMessage = [ 'role' => 'user' ];
410 $setContent( $newMessage, '' );
411 array_unshift( $processedMessages, $newMessage );
412 }
413
414 return $processedMessages;
415 }
416
417 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
418 public function stream_error_check( $data ) {
419 if ( strpos( $data, 'error' ) === false ) {
420 return;
421 }
422
423 $data = trim( $data );
424 $jsonPart = $data;
425 if ( strpos( $jsonPart, 'data:' ) === 0 ) {
426 $jsonPart = trim( substr( $jsonPart, strlen( 'data:' ) ) );
427 }
428
429 $json = json_decode( $jsonPart, true );
430 if ( json_last_error() !== JSON_ERROR_NONE ) {
431 return; // not valid JSON, nothing to do
432 }
433 // 1. OpenAI style: { error: {...} }
434 $error = null;
435 if ( isset( $json['error'] ) ) {
436 $error = $json['error'];
437 }
438 // 2. Google style: [ { error: {...} } ]
439 else if ( is_array( $json ) ) {
440 foreach ( $json as $item ) {
441 if ( isset( $item['error'] ) ) {
442 $error = $item['error'];
443 break;
444 }
445 }
446 }
447 // 3. Some APIs return { type: "error", message: ... }
448 else if ( isset( $json['type'] ) && $json['type'] === 'error' ) {
449 $error = $json;
450 }
451
452 if ( is_null( $error ) ) {
453 return;
454 }
455
456 $message = $error['message'] ?? ( is_string( $error ) ? $error : null );
457 $code = $error['code'] ?? null;
458 // Google uses "status" instead of "type" – accept both
459 $type = $error['type'] ?? ( $error['status'] ?? null );
460 if ( is_null( $message ) ) {
461 throw new Exception( 'Unknown error (stream_error_check).' );
462 }
463
464 $errorMessage = "Error: $message";
465 if ( !is_null( $code ) ) {
466 $errorMessage .= " ($code)";
467 }
468 if ( !is_null( $type ) ) {
469 $errorMessage .= " ($type)";
470 }
471
472 throw new Exception( $errorMessage );
473 }
474
475 protected function init_debug_mode( $query ) {
476 // Check if server debug mode or event logs are enabled in settings
477 $this->currentDebugMode = ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'server_debug_mode' ) ) || $this->core->get_option( 'event_logs' );
478 $this->currentQuery = $query;
479 }
480
481 public function stream_handler( $handle, $args, $url ) {
482 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
483 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
484
485 // TODO: This is breaking the response. We need to find a way to handle the headers.
486 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function ( $curl, $header ) {
487 // $length = strlen( $header );
488 // $this->streamHeaders[] = $header;
489 // $this->stream_header_handler( $header );
490 // return $length;
491 // });
492
493 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) use ( $url ) {
494 $length = strlen( $data );
495
496 // Log streaming data if queries debug is enabled
497 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
498 static $logged_url = false;
499 if ( $queries_debug && !$logged_url ) {
500 error_log( '[AI Engine Queries] Streaming from: ' . $url );
501 $logged_url = true;
502 }
503
504 // Bufferize the unfinished stream (if it's the case)
505 $this->streamTemporaryBuffer .= $data;
506 $this->streamBuffer .= $data;
507
508 // Error Management
509 $this->stream_error_check( $this->streamBuffer );
510
511 $lines = explode( "\n", $this->streamTemporaryBuffer );
512 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
513 $this->streamTemporaryBuffer = array_pop( $lines );
514 }
515 else {
516 $this->streamTemporaryBuffer = '';
517 }
518
519 foreach ( $lines as $line ) {
520 if ( $line === '' ) {
521 continue;
522 }
523 if ( strpos( $line, 'data:' ) === 0 ) {
524 $line = trim( substr( $line, 5 ) );
525 $json = json_decode( trim( $line ), true );
526
527 if ( json_last_error() === JSON_ERROR_NONE ) {
528 // Log individual streaming event if queries debug is enabled
529 static $event_count = 0;
530 if ( $queries_debug && $event_count < 10 ) {
531 // Log only the event type and key data, not the entire response
532 $event_log = [
533 'type' => $json['type'] ?? 'unknown'
534 ];
535
536 // Add specific details based on event type
537 if ( isset( $json['type'] ) ) {
538 if ( $json['type'] === 'response.output_item.added' && isset( $json['item'] ) ) {
539 $event_log['item_type'] = $json['item']['type'] ?? 'unknown';
540 $event_log['name'] = $json['item']['name'] ?? null;
541 $event_log['call_id'] = $json['item']['call_id'] ?? null;
542 }
543 elseif ( strpos( $json['type'], 'response.function_call' ) === 0 ) {
544 $event_log['call_id'] = $json['call_id'] ?? $json['item_id'] ?? null;
545 }
546 elseif ( $json['type'] === 'response.output_item.done' && isset( $json['item'] ) ) {
547 $event_log['item_type'] = $json['item']['type'] ?? 'unknown';
548 if ( isset( $json['item']['call_id'] ) ) {
549 $event_log['call_id'] = $json['item']['call_id'];
550 }
551 }
552 }
553
554 error_log( '[AI Engine Queries] Event: ' . json_encode( $event_log ) );
555 $event_count++;
556 }
557
558 $content = $this->stream_data_handler( $json );
559 if ( !is_null( $content ) ) {
560
561 // Check if content is an Event object
562 if ( is_object( $content ) && $content instanceof Meow_MWAI_Event ) {
563 // For Event objects, pass the object directly to callback
564 // Don't accumulate in streamContent as it's not regular text
565 call_user_func( $this->streamCallback, $content );
566 }
567 else {
568 // For regular string content
569
570 // TO CHECK: Not sure why we need to do this to make sure there is a line return in the chatbot
571 // If we don't do this, HuggingFace streams "\n" as a token without anything else, and the
572 // chatbot doesn't display it.
573 if ( $content === "\n" ) {
574 $content = " \n";
575 }
576
577 $this->streamContent .= $content;
578 call_user_func( $this->streamCallback, $content );
579 }
580 }
581 }
582 else if ( $line !== '[DONE]' && !empty( $line ) ) {
583 $this->streamTemporaryBuffer .= $line . "\n";
584 }
585 }
586 }
587 return $length;
588 } );
589 }
590
591 protected function stream_header_handler( $header ) {
592
593 }
594
595 protected function stream_data_handler( $json ) {
596 throw new Exception( 'Not implemented.' );
597 }
598
599 public function get_models() {
600 throw new Exception( 'Not implemented.' );
601 }
602
603 public function retrieve_models() {
604 throw new Exception( 'Not implemented.' );
605 }
606
607 public function run_completion_query( Meow_MWAI_Query_Base $query, $streamCallback = null ): Meow_MWAI_Reply {
608 throw new Exception( 'Not implemented.' );
609 }
610
611 public function run_assistant_query( Meow_MWAI_Query_Assistant $query, $streamCallback = null ): Meow_MWAI_Reply {
612 throw new Exception( 'Not implemented, or not supported in this version of AI Engine.' );
613 }
614
615 public function run_embedding_query( Meow_MWAI_Query_Base $query ) {
616 throw new Exception( 'Not implemented.' );
617 }
618
619 public function run_image_query( Meow_MWAI_Query_Base $query ) {
620 throw new Exception( 'Not implemented.' );
621 }
622
623 public function run_editimage_query( Meow_MWAI_Query_Base $query ) {
624 throw new Exception( 'Not implemented.' );
625 }
626
627 public function run_transcribe_query( Meow_MWAI_Query_Base $query ) {
628 throw new Exception( 'Not implemented.' );
629 }
630
631 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
632 throw new Exception( 'Not implemented.' );
633 }
634
635 /**
636 * Check the connection to the AI service.
637 * This should be a minimal, cost-free API call to verify credentials and connectivity.
638 *
639 * @return array {
640 * @type bool $success Whether the connection test was successful
641 * @type string $service The service name (e.g., 'OpenAI', 'Anthropic')
642 * @type string $message A human-readable message about the test result
643 * @type array $details Additional service-specific details
644 * @type string $error Error message if the test failed
645 * }
646 */
647 public function connection_check() {
648 throw new Exception( 'Connection check not implemented for this service.' );
649 }
650 }
651