PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.3
3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / reply.php
ai-engine / classes Last commit date
data 11 months ago engines 1 month ago exceptions 11 months ago modules 1 month ago query 1 month ago rest 1 month ago services 1 month ago admin.php 1 month ago api.php 1 month ago core.php 1 month ago discussion.php 11 months ago event.php 11 months ago init.php 7 months ago logging.php 11 months ago reply.php 1 month ago rest.php 1 month ago
reply.php
362 lines
1 <?php
2
3 class Meow_MWAI_Reply implements JsonSerializable {
4 public $id = null;
5 public $result = '';
6 public $results = [];
7 public $usage = [
8 'prompt_tokens' => 0,
9 'completion_tokens' => 0,
10 'total_tokens' => 0,
11 'price' => null,
12 'accuracy' => 'none', // 'none', 'estimated', 'tokens', 'price', 'full'
13 ];
14 public $query = null;
15 public $type = 'text';
16 public $model = null; // Actual model used by the API (may differ from query model)
17
18 // Code interpreter code (separate from main content)
19 public $contentCode = '';
20
21 // This is when models return a message that needs to be executed (functions, tools, etc)
22 public $needFeedbacks = [];
23 public $needClientActions = [];
24
25 public function __construct( $query = null ) {
26 $this->query = $query;
27 }
28
29 #[\ReturnTypeWillChange]
30 public function jsonSerialize() {
31 $isEmbedding = false;
32 $embeddingsDimensions = null;
33 $embedddingsMessage = null;
34 if ( is_array( $this->results ) && count( $this->results ) > 0 ) {
35 $isEmbedding = is_array( $this->results[0] );
36 if ( $isEmbedding ) {
37 $embeddingsDimensions = count( $this->results[0] );
38 $embedddingsMessage = "A $embeddingsDimensions-dimensional embedding was returned.";
39 }
40 }
41 $data = [
42 'result' => $isEmbedding ? $embedddingsMessage : $this->result,
43 'results' => $isEmbedding ? [] : $this->results,
44 'usage' => $this->usage,
45 'system' => [
46 'class' => get_class( $this ),
47 ]
48 ];
49 if ( !empty( $this->needFeedbacks ) ) {
50 $data['needFeedbacks'] = $this->needFeedbacks;
51 }
52 if ( !empty( $this->needClientActions ) ) {
53 $data['needClientActions'] = $this->needClientActions;
54 }
55 if ( !empty( $this->contentCode ) ) {
56 $data['contentCode'] = $this->contentCode;
57 }
58 return $data;
59 }
60
61 public function set_usage( $usage ) {
62 $this->usage = $usage;
63 }
64
65 public function set_usage_accuracy( $accuracy ) {
66 $this->usage['accuracy'] = $accuracy;
67 }
68
69 public function set_id( $id ) {
70 $this->id = $id;
71 }
72
73 public function set_type( $type ) {
74 $this->type = $type;
75 }
76
77 public function set_model( $model ) {
78 $this->model = $model;
79 }
80
81 public function get_total_tokens() {
82 return isset( $this->usage['total_tokens'] ) ? $this->usage['total_tokens'] : 0;
83 }
84
85 public function get_in_tokens( $query = null ) {
86 $in_tokens = isset( $this->usage['prompt_tokens'] ) ? $this->usage['prompt_tokens'] : 0;
87 if ( empty( $in_tokens ) && $query ) {
88 $in_tokens = $query->get_in_tokens();
89 }
90 return $in_tokens;
91 }
92
93 public function get_out_tokens() {
94 $out_tokens = isset( $this->usage['completion_tokens'] ) ? $this->usage['completion_tokens'] : 0;
95 if ( empty( $out_tokens ) ) {
96 // NOTE: Only estimate when result is actually text. Embedding replies hold a
97 // float vector, image replies hold URLs/arrays, etc. — running estimate_tokens()
98 // on those JSON-encodes the structure and produces huge bogus counts (a 3072-d
99 // Gemini embedding estimated as ~13k tokens for a 50-char input). Anything that
100 // isn't a string has no meaningful "output token" count, so return 0.
101 if ( !is_string( $this->result ) ) {
102 return 0;
103 }
104 $out_tokens = Meow_MWAI_Core::estimate_tokens( $this->result );
105 }
106 return $out_tokens;
107 }
108
109 public function get_price() {
110 // If it's not set return null, but it can be 0
111 if ( !isset( $this->usage['price'] ) ) {
112 return null;
113 }
114 return $this->usage['price'];
115 }
116
117 public function get_usage_accuracy() {
118 return $this->usage['accuracy'] ?? 'none';
119 }
120
121 /**
122 * Returns the metric count for this reply, preferring tokens. Falls back to
123 * images or seconds for non-token-billed providers (legacy Imagen/Replicate
124 * per-image, Whisper, Sora). Equivalent to (and aliased by) get_units().
125 */
126 public function get_tokens() {
127 if ( isset( $this->usage['total_tokens'] ) ) {
128 return $this->usage['total_tokens'];
129 }
130 else if ( isset( $this->usage['images'] ) ) {
131 return $this->usage['images'];
132 }
133 else if ( isset( $this->usage['seconds'] ) ) {
134 return $this->usage['seconds'];
135 }
136 return null;
137 }
138
139 /**
140 * Legacy alias for get_tokens(). Kept indefinitely — third-party code
141 * reachable via the mwai_ai_reply filter may call it.
142 */
143 public function get_units() {
144 return $this->get_tokens();
145 }
146
147 public function get_type() {
148 return $this->type;
149 }
150
151 public function set_reply( $reply ) {
152 $this->result = $reply;
153 $this->results[] = [ $reply ];
154 }
155
156 public function replace( $search, $replace ) {
157 $this->result = str_replace( $search, $replace, $this->result );
158 $this->results = array_map( function ( $result ) use ( $search, $replace ) {
159 return str_replace( $search, $replace, $result );
160 }, $this->results );
161 }
162
163 private function extract_arguments( $funcArgs ) {
164 $finalArgs = [];
165 if ( is_string( $funcArgs ) ) {
166 $arguments = trim( str_replace( "\n", '', $funcArgs ) );
167 if ( substr( $arguments, 0, 1 ) == '{' ) {
168 $arguments = json_decode( $arguments, true );
169 $finalArgs = $arguments;
170 }
171 }
172 else if ( is_array( $funcArgs ) ) {
173 $finalArgs = $funcArgs;
174 }
175 return $finalArgs;
176 }
177
178 /**
179 * Set the choices from OpenAI as the results.
180 * The last (or only) result is set as the result.
181 * @param array $choices ID of the model to use.
182 */
183 public function set_choices( $choices, $rawMessage = null ) {
184 $this->results = [];
185
186 // Initialize feedback arrays at the start to accumulate across all choices
187 // This is important for engines like Google that split multiple function calls
188 // into separate choices
189 $this->needFeedbacks = [];
190 $this->needClientActions = [];
191
192 if ( is_array( $choices ) ) {
193 foreach ( $choices as $choice ) {
194
195 // It's chat completion
196 if ( isset( $choice['message'] ) ) {
197
198 // It's text content
199 if ( isset( $choice['message']['content'] ) ) {
200 $content = trim( $choice['message']['content'] );
201 $this->results[] = $content;
202 $this->result = $content;
203 }
204
205 // It's a tool call (OpenAI-style and Anthropic-style)
206 $toolCalls = [];
207 if ( isset( $choice['message']['tool_calls'] ) ) {
208 $tools = $choice['message']['tool_calls'];
209 foreach ( $tools as $tool ) {
210 if ( $tool['type'] === 'function' ) {
211 $toolCall = [
212 'toolId' => $tool['id'],
213 //'mode' => 'interactive',
214 'type' => 'tool_call',
215 'name' => trim( $tool['function']['name'] ),
216 'arguments' => $this->extract_arguments( $tool['function']['arguments'] ),
217 // Represent the original message that triggered the function call
218 'rawMessage' => $rawMessage ? $rawMessage : ( isset( $choice['_rawMessage'] ) ? $choice['_rawMessage'] : $choice['message'] ),
219 ];
220 $toolCalls[] = $toolCall;
221 }
222 }
223 }
224
225 // If it's a function call (Open-AI style; usually for a final execution)
226 if ( isset( $choice['message']['function_call'] ) ) {
227 $content = $choice['message']['function_call'];
228 $name = trim( $content['name'] );
229 $args = $content['arguments'] ?? $content['args'] ?? null;
230 $toolCalls[] = [
231 'toolId' => null,
232 'mode' => 'static',
233 'type' => 'function_call',
234 'name' => $name,
235 'arguments' => $this->extract_arguments( $args ),
236 'rawMessage' => $rawMessage ? $rawMessage : ( isset( $choice['_rawMessage'] ) ? $choice['_rawMessage'] : $choice['message'] ),
237 ];
238 }
239
240 // Deep copy tool calls BEFORE adding function references
241 // This prevents the "Duplicate value for 'tool_call_id'" error
242 // when the same function is called multiple times
243 // Note: We need to preserve the toolId for each tool call
244 if ( !empty( $toolCalls ) ) {
245 $toolCalls = json_decode( json_encode( $toolCalls ), true );
246 }
247
248 // Resolve the original function from the query
249 if ( !empty( $toolCalls ) ) {
250 foreach ( $toolCalls as &$toolCall ) {
251 if ( $toolCall['type'] !== 'function_call' && $toolCall['type'] !== 'tool_call' ) {
252 continue;
253 }
254 foreach ( $this->query->functions as $function ) {
255 if ( $function->name == $toolCall['name'] ) {
256 $toolCall['function'] = $function;
257 break;
258 }
259 }
260 }
261 // IMPORTANT: Unset the reference to avoid PHP's foreach reference bug
262 unset( $toolCall );
263 }
264
265 // Add tool calls to existing arrays instead of resetting them
266 // This is crucial for engines like Google that create multiple choices
267 // for multiple function calls in a single response
268 foreach ( $toolCalls as $tcIdx => $toolCall ) {
269 if ( $toolCall['function']->target !== 'js' ) {
270 $this->needFeedbacks[] = $toolCall;
271 }
272 else if ( $toolCall['function']->target === 'js' ) {
273 $this->needClientActions[] = $toolCall;
274 }
275 }
276 }
277
278 // It's text completion
279 else if ( isset( $choice['text'] ) ) {
280
281 // TODO: Assistants return an array (so actually not really a text completion)
282 // We should probably make this clearer and analyze all the outputs from different endpoints.
283 if ( is_array( $choice['text'] ) ) {
284 $text = trim( $choice['text']['value'] );
285 $this->results[] = $text;
286 $this->result = $text;
287 }
288 else {
289 $text = trim( $choice['text'] );
290 $this->results[] = $text;
291 $this->result = $text;
292 }
293 }
294
295 // It's url/image
296 else if ( isset( $choice['url'] ) ) {
297 $url = trim( $choice['url'] );
298 $this->results[] = $url;
299 $this->result = $url;
300 }
301 else if ( isset( $choice['b64_json'] ) ) {
302 // In that case we need to create a temporary file in WordPress to store the image, and return the URL for it.
303 global $mwai_core;
304
305 // Check if the query has explicitly disabled local download
306 if ( !empty( $this->query ) && $this->query instanceof Meow_MWAI_Query_Image && $this->query->localDownload === null ) {
307 // Query explicitly doesn't want local download, save as temporary upload
308 $localDownload = 'uploads';
309 $expiry = 1 * HOUR_IN_SECONDS; // 1 hour for temporary images
310 }
311 else {
312 // Use the user's AI-generated image settings
313 $localDownload = $mwai_core->get_option( 'image_local_download' );
314 $expiry = (int) $mwai_core->get_option( 'image_expires_download' );
315 }
316
317 // The expiry is already in seconds
318 $ttl = $expiry;
319
320 // Use 'library' or 'uploads' based on user settings
321 $target = ( $localDownload === 'library' ) ? 'library' : 'uploads';
322
323 // Prepare metadata similar to regular image queries
324 $metadata = [];
325 if ( !empty( $this->query ) ) {
326 $metadata['query_envId'] = $this->query->envId ?? null;
327 $metadata['query_session'] = $this->query->session ?? null;
328 $metadata['query_model'] = $this->query->model ?? 'gpt-image-1';
329 }
330
331 $url = $mwai_core->files->save_temp_image_from_b64( $choice['b64_json'], 'generated', $ttl, $target, $metadata );
332 if ( is_wp_error( $url ) ) {
333 return $url;
334 }
335 $this->results[] = $url;
336
337 // For chatbot display, append image markdown to the result
338 if ( !empty( $this->result ) ) {
339 $this->result .= "\n\n";
340 }
341 $this->result .= "![Generated Image]($url)";
342 }
343
344 // It's embedding
345 else if ( isset( $choice['embedding'] ) ) {
346 $content = $choice['embedding'];
347 $this->results[] = $content;
348 $this->result = $content;
349 }
350 }
351 }
352 else {
353 $this->result = $choices;
354 $this->results[] = $choices;
355 }
356 }
357
358 public function toJson() {
359 return json_encode( $this );
360 }
361 }
362