PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.2
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 6 months ago chatml.php 6 months ago core.php 7 months ago factory.php 8 months ago google.php 6 months ago mistral.php 6 months ago open-router.php 6 months ago openai.php 6 months ago perplexity.php 6 months ago replicate.php 6 months ago
core.php
723 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 // Prevent infinite loops - each function call reduces maxDepth by 1
201 if ( $maxDepth <= 0 ) {
202 // Build call stack for better debugging
203 $callStack = [];
204 foreach ( $reply->needFeedbacks as $feedback ) {
205 $callStack[] = $feedback['name'] ?? 'unknown';
206 }
207
208 throw Meow_MWAI_FunctionCallException::loop_detected(
209 $query->_maxDepthConfigured ?? 5, // Use configured max depth
210 $callStack
211 );
212 }
213
214 // Create a feedback query if we're not already in one
215 // This wraps the original query with function execution results
216 if ( !( $query instanceof Meow_MWAI_Query_AssistFeedback ) && !( $query instanceof Meow_MWAI_Query_Feedback ) ) {
217 $queryClass = $query instanceof Meow_MWAI_Query_Assistant ?
218 Meow_MWAI_Query_AssistFeedback::class : Meow_MWAI_Query_Feedback::class;
219 // Note: $reply->query contains the original query that produced this reply
220 $query = new $queryClass( $reply, $reply->query );
221 }
222
223 // Validate that all function calls have proper function definitions
224 foreach ( $reply->needFeedbacks as $needFeedback ) {
225 if ( !isset( $needFeedback['function'] ) ) {
226 $functionName = $needFeedback['name'] ?? 'unknown';
227 $availableFunctions = array_map( function ( $f ) { return $f->name; }, $query->functions );
228
229 throw new Exception( sprintf(
230 "Function '%s' not found in query functions. Available functions: %s",
231 $functionName,
232 implode( ', ', $availableFunctions )
233 ) );
234 }
235 }
236
237 // Group function calls by their source message to maintain proper context
238 // This ensures related function calls are processed together
239 $feedback_blocks = [];
240
241 // Special handling for Responses API - group all function calls together
242 // Check if we're using Responses API by looking at the query's previous response ID or reply ID
243 $isResponsesApi = false;
244
245 // Method 1: Check if query has a previous response ID from Responses API
246 if ( !empty( $query->previousResponseId ) && $this->core->responseIdManager->is_valid_for_responses_api( $query->previousResponseId ) ) {
247 $isResponsesApi = true;
248 }
249
250 // Method 2: Check if the reply has a Responses API response ID
251 if ( !$isResponsesApi && !empty( $reply->id ) && $this->core->responseIdManager->is_valid_for_responses_api( $reply->id ) ) {
252 $isResponsesApi = true;
253 }
254
255 // Method 3: Check the model tags for 'responses' tag
256 if ( !$isResponsesApi && !empty( $query->model ) ) {
257 $modelInfo = $this->retrieve_model_info( $query->model );
258 if ( $modelInfo && !empty( $modelInfo['tags'] ) && in_array( 'responses', $modelInfo['tags'] ) ) {
259 // Also check if Responses API is enabled in settings
260 $responsesApiEnabled = $this->core->get_option( 'ai_responses_api' ) ?? true;
261 if ( $responsesApiEnabled ) {
262 $isResponsesApi = true;
263 }
264 }
265 }
266
267 // Method 4: For OpenAI engine, check if we're already using Responses API
268 // This is important for models that use Responses API but don't have the tag
269 if ( !$isResponsesApi && method_exists( $this, 'should_use_responses_api' ) ) {
270 // This is an OpenAI engine, check if it should use Responses API
271 $isResponsesApi = $this->should_use_responses_api( $query->model );
272 }
273
274 // Debug: Log grouping information
275 if ( $queries_debug ) {
276 error_log( '[AI Engine Queries] Grouping ' . count( $reply->needFeedbacks ) . ' function calls' );
277 error_log( '[AI Engine Queries] Is Responses API: ' . ( $isResponsesApi ? 'yes' : 'no' ) );
278 error_log( '[AI Engine Queries] Detection methods:' );
279 error_log( '[AI Engine Queries] - previousResponseId: ' . ( $query->previousResponseId ?? 'null' ) );
280 error_log( '[AI Engine Queries] - reply->id: ' . ( $reply->id ?? 'null' ) );
281 error_log( '[AI Engine Queries] - model: ' . ( $query->model ?? 'null' ) );
282 error_log( '[AI Engine Queries] - method_exists should_use_responses_api: ' . ( method_exists( $this, 'should_use_responses_api' ) ? 'yes' : 'no' ) );
283 error_log( '[AI Engine Queries] - engine class: ' . get_class( $this ) );
284 if ( $isResponsesApi ) {
285 error_log( '[AI Engine Queries] All function calls will be grouped together for Responses API' );
286 }
287 }
288
289 foreach ( $reply->needFeedbacks as $idx => $needFeedback ) {
290 // For Responses API, use a single key to group all function calls together
291 $rawMessageKey = md5( serialize( $needFeedback['rawMessage'] ) );
292
293 if ( $queries_debug ) {
294 error_log( '[AI Engine Queries] Function call ' . $idx . ': ' . $needFeedback['name'] . ' (key: ' . substr( $rawMessageKey, 0, 8 ) . ')' );
295 }
296
297 // Initialize the feedback block for this rawMessage if it hasn't been initialized yet
298 if ( !isset( $feedback_blocks[$rawMessageKey] ) ) {
299 $feedback_blocks[$rawMessageKey] = [
300 'rawMessage' => $needFeedback['rawMessage'],
301 'feedbacks' => []
302 ];
303 }
304
305 // Get the value related to this feedback (usually, a function call)
306 $value = apply_filters( 'mwai_ai_feedback', null, $needFeedback, $reply );
307
308 if ( $value === null ) {
309 // Check if the function handler exists
310 if ( !has_filter( 'mwai_ai_feedback' ) ) {
311 Meow_MWAI_Logging::error(
312 Meow_MWAI_FunctionCallException::missing_function_handler(
313 $needFeedback['name']
314 )->getMessage()
315 );
316 }
317 else {
318 Meow_MWAI_Logging::warn( "The returned value for '{$needFeedback['name']}' was null." );
319 }
320 $value = '[NO VALUE RETURNED - DO NOT SHOW THIS]';
321 }
322
323 // Emit "Got result" event and log for debugging
324 if ( $this->currentDebugMode ) {
325 // Format the result preview
326 $resultPreview = is_array( $value ) ? json_encode( $value ) : (string) $value;
327 if ( strlen( $resultPreview ) > 100 ) {
328 $resultPreview = substr( $resultPreview, 0, 100 ) . '...';
329 }
330
331 // Log the function result for debugging
332 Meow_MWAI_Logging::log( "Function '{$needFeedback['name']}' returned: " . $resultPreview );
333
334 // Emit function result event if we have a callback
335 if ( !empty( $streamCallback ) ) {
336 // Load event helper if not already loaded
337 if ( !class_exists( 'Meow_MWAI_Event' ) ) {
338 require_once MWAI_PATH . '/classes/event.php';
339 }
340
341 $functionName = $needFeedback['name'];
342
343 $event = Meow_MWAI_Event::function_result( $functionName )
344 ->set_metadata( 'result', $resultPreview )
345 ->set_metadata( 'tool_id', $needFeedback['toolId'] ?? null );
346 call_user_func( $streamCallback, $event );
347 }
348 }
349
350 // Add the feedback information to the appropriate feedback block
351 $feedback_blocks[$rawMessageKey]['feedbacks'][] = [
352 'request' => $needFeedback, // TODO: Meow_MWAI_Feedback_Request
353 'reply' => [ 'value' => $value ] // TODO: Meow_MWAI_Feedback_Reply
354 ];
355 }
356
357 $query->clear_feedback_blocks();
358 foreach ( $feedback_blocks as $feedback_block ) {
359 $query->add_feedback_block( $feedback_block );
360 }
361
362 // Log feedback query if debug is enabled
363 if ( $queries_debug ) {
364 error_log( '[AI Engine Queries] Created ' . count( $feedback_blocks ) . ' feedback blocks from ' . count( $reply->needFeedbacks ) . ' function calls' );
365 foreach ( $feedback_blocks as $key => $block ) {
366 error_log( '[AI Engine Queries] Block ' . substr( $key, 0, 8 ) . ' has ' . count( $block['feedbacks'] ) . ' feedbacks' );
367 }
368 }
369
370 // Run the feedback query
371 $reply = $this->run( $query, $streamCallback, $maxDepth - 1 );
372 }
373
374 return $reply;
375 }
376
377 public function retrieve_model_info( $model ) {
378 $models = $this->get_models();
379 foreach ( $models as $currentModel ) {
380 if ( $currentModel['model'] === $model ) {
381 return $currentModel;
382 }
383 }
384 return false;
385 }
386
387 public function final_checks( Meow_MWAI_Query_Base $query ) {
388 $query->final_checks();
389 //$found = false;
390
391 // Check if the model is available, except if it's an assistant
392 if ( !( $query instanceof Meow_MWAI_Query_Assistant ) ) {
393 // TODO: Avoid checking on the finetuned models for now.
394 if ( substr( $query->model, 0, 3 ) === 'ft:' ) {
395 return;
396 }
397 $model_info = $this->retrieve_model_info( $query->model );
398 if ( $model_info === false ) {
399 // Provide a more helpful error message for embeddings queries without a configured environment
400 if ( $query instanceof Meow_MWAI_Query_Embed && empty( $query->envId ) ) {
401 throw new Exception( __( 'No embeddings environment is configured. Please go to Settings > Default Environments for AI > Embeddings and select an environment.', 'ai-engine' ) );
402 }
403 throw new Exception( sprintf( __( "The model '%s' is not available.", 'ai-engine' ), $query->model ) );
404 }
405 if ( isset( $model_info['mode'] ) ) {
406 $query->mode = $model_info['mode'];
407 }
408 }
409 }
410
411 // Streamline the messages:
412 // - Concatenate consecutive model messages into a single message for the model role
413 // - Make sure the first message is a user message
414 // - Make sure the last message is a user message
415 protected function streamline_messages( $messages, $systemRole = 'assistant', $messageType = 'content' ) {
416 $processedMessages = [];
417 $lastRole = '';
418 $concatenatedText = '';
419
420 // Determine the way to access message content based on messageType
421 $getContent = function ( $message ) use ( $messageType ) {
422 if ( $messageType == 'parts' ) {
423 return $message['parts'][0]['text'];
424 }
425 else { // Default to 'content'
426 return $message['content'];
427 }
428 };
429
430 // Set content to a message depending on the messageType
431 $setContent = function ( &$message, $content ) use ( $messageType ) {
432 if ( $messageType == 'parts' ) {
433 $message['parts'] = [['text' => $content]];
434 }
435 else { // Default to 'content'
436 $message['content'] = $content;
437 }
438 };
439
440 // Concatenate consecutive model messages into a single message for the model role
441 foreach ( $messages as $message ) {
442 if ( $message['role'] == $systemRole ) {
443 if ( $lastRole == $systemRole ) {
444 $concatenatedText .= "\n" . $getContent( $message );
445 }
446 else {
447 if ( $concatenatedText !== '' ) {
448 $newMessage = [ 'role' => $systemRole ];
449 $setContent( $newMessage, $concatenatedText );
450 $processedMessages[] = $newMessage;
451 }
452 $concatenatedText = $getContent( $message );
453 }
454 }
455 else {
456 if ( $lastRole == $systemRole ) {
457 $newMessage = [ 'role' => $systemRole ];
458 $setContent( $newMessage, $concatenatedText );
459 $processedMessages[] = $newMessage;
460 $concatenatedText = '';
461 }
462 $processedMessages[] = $message;
463 }
464 $lastRole = $message['role'];
465 }
466 if ( $lastRole == $systemRole && $concatenatedText !== '' ) {
467 $newMessage = [ 'role' => $systemRole ];
468 $setContent( $newMessage, $concatenatedText );
469 $processedMessages[] = $newMessage;
470 }
471
472 // Make sure the last message is a user message, if not, throw an exception
473 if ( end( $processedMessages )['role'] !== 'user' ) {
474 throw new Exception( __( 'The last message must be a user message.', 'ai-engine' ) );
475 }
476
477 // Make sure the first message is a user message, if not, add an empty user message
478 if ( $processedMessages[0]['role'] !== 'user' ) {
479 $newMessage = [ 'role' => 'user' ];
480 $setContent( $newMessage, '' );
481 array_unshift( $processedMessages, $newMessage );
482 }
483
484 return $processedMessages;
485 }
486
487 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
488 public function stream_error_check( $data ) {
489 if ( strpos( $data, 'error' ) === false ) {
490 return;
491 }
492
493 $data = trim( $data );
494 $jsonPart = $data;
495 if ( strpos( $jsonPart, 'data:' ) === 0 ) {
496 $jsonPart = trim( substr( $jsonPart, strlen( 'data:' ) ) );
497 }
498
499 $json = json_decode( $jsonPart, true );
500 if ( json_last_error() !== JSON_ERROR_NONE ) {
501 return; // not valid JSON, nothing to do
502 }
503 // 1. OpenAI style: { error: {...} }
504 $error = null;
505 if ( isset( $json['error'] ) ) {
506 $error = $json['error'];
507 }
508 // 2. Google style: [ { error: {...} } ]
509 else if ( is_array( $json ) ) {
510 foreach ( $json as $item ) {
511 if ( isset( $item['error'] ) ) {
512 $error = $item['error'];
513 break;
514 }
515 }
516 }
517 // 3. Some APIs return { type: "error", message: ... }
518 else if ( isset( $json['type'] ) && $json['type'] === 'error' ) {
519 $error = $json;
520 }
521
522 if ( is_null( $error ) ) {
523 return;
524 }
525
526 $message = $error['message'] ?? ( is_string( $error ) ? $error : null );
527 $code = $error['code'] ?? null;
528 // Google uses "status" instead of "type" – accept both
529 $type = $error['type'] ?? ( $error['status'] ?? null );
530 if ( is_null( $message ) ) {
531 throw new Exception( 'Unknown error (stream_error_check).' );
532 }
533
534 $errorMessage = "Error: $message";
535 if ( !is_null( $code ) ) {
536 $errorMessage .= " ($code)";
537 }
538 if ( !is_null( $type ) ) {
539 $errorMessage .= " ($type)";
540 }
541
542 throw new Exception( $errorMessage );
543 }
544
545 protected function init_debug_mode( $query ) {
546 // Check if server debug mode or event logs are enabled in settings
547 $this->currentDebugMode = ( $this->core->get_option( 'module_devtools' ) && $this->core->get_option( 'server_debug_mode' ) ) || $this->core->get_option( 'event_logs' );
548 $this->currentQuery = $query;
549 }
550
551 public function stream_handler( $handle, $args, $url ) {
552 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
553 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
554
555 // TODO: This is breaking the response. We need to find a way to handle the headers.
556 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function ( $curl, $header ) {
557 // $length = strlen( $header );
558 // $this->streamHeaders[] = $header;
559 // $this->stream_header_handler( $header );
560 // return $length;
561 // });
562
563 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) use ( $url ) {
564 $length = strlen( $data );
565
566 // Log streaming data if queries debug is enabled
567 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
568 static $logged_url = false;
569 if ( $queries_debug && !$logged_url ) {
570 error_log( '[AI Engine Queries] Streaming from: ' . $url );
571 $logged_url = true;
572 }
573
574 // Bufferize the unfinished stream (if it's the case)
575 $this->streamTemporaryBuffer .= $data;
576 $this->streamBuffer .= $data;
577
578 // Error Management
579 $this->stream_error_check( $this->streamBuffer );
580
581 $lines = explode( "\n", $this->streamTemporaryBuffer );
582 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
583 $this->streamTemporaryBuffer = array_pop( $lines );
584 }
585 else {
586 $this->streamTemporaryBuffer = '';
587 }
588
589 foreach ( $lines as $line ) {
590 if ( $line === '' ) {
591 continue;
592 }
593 if ( strpos( $line, 'data:' ) === 0 ) {
594 $line = trim( substr( $line, 5 ) );
595 $json = json_decode( trim( $line ), true );
596
597 if ( json_last_error() === JSON_ERROR_NONE ) {
598 // Log individual streaming event if queries debug is enabled
599 static $event_count = 0;
600 if ( $queries_debug && $event_count < 10 ) {
601 // Log only the event type and key data, not the entire response
602 $event_log = [
603 'type' => $json['type'] ?? 'unknown'
604 ];
605
606 // Add specific details based on event type
607 if ( isset( $json['type'] ) ) {
608 if ( $json['type'] === 'response.output_item.added' && isset( $json['item'] ) ) {
609 $event_log['item_type'] = $json['item']['type'] ?? 'unknown';
610 $event_log['name'] = $json['item']['name'] ?? null;
611 $event_log['call_id'] = $json['item']['call_id'] ?? null;
612 }
613 elseif ( strpos( $json['type'], 'response.function_call' ) === 0 ) {
614 $event_log['call_id'] = $json['call_id'] ?? $json['item_id'] ?? null;
615 }
616 elseif ( $json['type'] === 'response.output_item.done' && isset( $json['item'] ) ) {
617 $event_log['item_type'] = $json['item']['type'] ?? 'unknown';
618 if ( isset( $json['item']['call_id'] ) ) {
619 $event_log['call_id'] = $json['item']['call_id'];
620 }
621 }
622 }
623
624 error_log( '[AI Engine Queries] Event: ' . json_encode( $event_log ) );
625 $event_count++;
626 }
627
628 $content = $this->stream_data_handler( $json );
629 if ( !is_null( $content ) ) {
630
631 // Check if content is an Event object
632 if ( is_object( $content ) && $content instanceof Meow_MWAI_Event ) {
633 // For Event objects, pass the object directly to callback
634 // Don't accumulate in streamContent as it's not regular text
635 call_user_func( $this->streamCallback, $content );
636 }
637 else if ( !empty( $content ) || $content === '0' ) {
638 // For regular string content - only process non-empty strings (but allow '0')
639 // TODO: This fixes an issue where empty strings were causing [Object] to appear in the chatbot during streaming.
640 // If no issues are reported after November 2025, this TODO comment can be removed (keep the code as-is).
641
642 // TO CHECK: Not sure why we need to do this to make sure there is a line return in the chatbot
643 // If we don't do this, HuggingFace streams "\n" as a token without anything else, and the
644 // chatbot doesn't display it.
645 if ( $content === "\n" ) {
646 $content = " \n";
647 }
648
649 $this->streamContent .= $content;
650 call_user_func( $this->streamCallback, $content );
651 }
652 }
653 }
654 else if ( $line !== '[DONE]' && !empty( $line ) ) {
655 $this->streamTemporaryBuffer .= $line . "\n";
656 }
657 }
658 }
659 return $length;
660 } );
661 }
662
663 protected function stream_header_handler( $header ) {
664
665 }
666
667 protected function stream_data_handler( $json ) {
668 throw new Exception( 'Not implemented.' );
669 }
670
671 public function get_models() {
672 throw new Exception( 'Not implemented.' );
673 }
674
675 public function retrieve_models() {
676 throw new Exception( 'Not implemented.' );
677 }
678
679 public function run_completion_query( Meow_MWAI_Query_Base $query, $streamCallback = null ): Meow_MWAI_Reply {
680 throw new Exception( 'Not implemented.' );
681 }
682
683 public function run_assistant_query( Meow_MWAI_Query_Assistant $query, $streamCallback = null ): Meow_MWAI_Reply {
684 throw new Exception( 'Not implemented, or not supported in this version of AI Engine.' );
685 }
686
687 public function run_embedding_query( Meow_MWAI_Query_Base $query ) {
688 throw new Exception( 'Not implemented.' );
689 }
690
691 public function run_image_query( Meow_MWAI_Query_Base $query, $streamCallback = null ) {
692 throw new Exception( 'Not implemented.' );
693 }
694
695 public function run_editimage_query( Meow_MWAI_Query_Base $query ) {
696 throw new Exception( 'Not implemented.' );
697 }
698
699 public function run_transcribe_query( Meow_MWAI_Query_Base $query ) {
700 throw new Exception( 'Not implemented.' );
701 }
702
703 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
704 throw new Exception( 'Not implemented.' );
705 }
706
707 /**
708 * Check the connection to the AI service.
709 * This should be a minimal, cost-free API call to verify credentials and connectivity.
710 *
711 * @return array {
712 * @type bool $success Whether the connection test was successful
713 * @type string $service The service name (e.g., 'OpenAI', 'Anthropic')
714 * @type string $message A human-readable message about the test result
715 * @type array $details Additional service-specific details
716 * @type string $error Error message if the test failed
717 * }
718 */
719 public function connection_check() {
720 throw new Exception( 'Connection check not implemented for this service.' );
721 }
722 }
723