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