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