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