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