PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / trunk
AI Engine – The Chatbot, AI Framework & MCP for WordPress vtrunk
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 21 hours ago exceptions 11 months ago modules 21 hours ago query 1 week ago rest 1 month ago services 21 hours ago admin.php 1 month ago api.php 1 month ago core.php 5 days ago discussion.php 11 months ago event.php 11 months ago init.php 7 months ago logging.php 11 months ago reply.php 3 weeks ago rest.php 1 month ago
reply.php
367 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 // Engine-specific extras kept on the reply during a single request (e.g. Google
22 // stores generated images, thoughts and grounding metadata here). Declared so PHP
23 // 8.2+ does not warn about dynamic property creation.
24 public $extraData = [];
25
26 // This is when models return a message that needs to be executed (functions, tools, etc)
27 public $needFeedbacks = [];
28 public $needClientActions = [];
29
30 public function __construct( $query = null ) {
31 $this->query = $query;
32 }
33
34 #[\ReturnTypeWillChange]
35 public function jsonSerialize() {
36 $isEmbedding = false;
37 $embeddingsDimensions = null;
38 $embedddingsMessage = null;
39 if ( is_array( $this->results ) && count( $this->results ) > 0 ) {
40 $isEmbedding = is_array( $this->results[0] );
41 if ( $isEmbedding ) {
42 $embeddingsDimensions = count( $this->results[0] );
43 $embedddingsMessage = "A $embeddingsDimensions-dimensional embedding was returned.";
44 }
45 }
46 $data = [
47 'result' => $isEmbedding ? $embedddingsMessage : $this->result,
48 'results' => $isEmbedding ? [] : $this->results,
49 'usage' => $this->usage,
50 'system' => [
51 'class' => get_class( $this ),
52 ]
53 ];
54 if ( !empty( $this->needFeedbacks ) ) {
55 $data['needFeedbacks'] = $this->needFeedbacks;
56 }
57 if ( !empty( $this->needClientActions ) ) {
58 $data['needClientActions'] = $this->needClientActions;
59 }
60 if ( !empty( $this->contentCode ) ) {
61 $data['contentCode'] = $this->contentCode;
62 }
63 return $data;
64 }
65
66 public function set_usage( $usage ) {
67 $this->usage = $usage;
68 }
69
70 public function set_usage_accuracy( $accuracy ) {
71 $this->usage['accuracy'] = $accuracy;
72 }
73
74 public function set_id( $id ) {
75 $this->id = $id;
76 }
77
78 public function set_type( $type ) {
79 $this->type = $type;
80 }
81
82 public function set_model( $model ) {
83 $this->model = $model;
84 }
85
86 public function get_total_tokens() {
87 return isset( $this->usage['total_tokens'] ) ? $this->usage['total_tokens'] : 0;
88 }
89
90 public function get_in_tokens( $query = null ) {
91 $in_tokens = isset( $this->usage['prompt_tokens'] ) ? $this->usage['prompt_tokens'] : 0;
92 if ( empty( $in_tokens ) && $query ) {
93 $in_tokens = $query->get_in_tokens();
94 }
95 return $in_tokens;
96 }
97
98 public function get_out_tokens() {
99 $out_tokens = isset( $this->usage['completion_tokens'] ) ? $this->usage['completion_tokens'] : 0;
100 if ( empty( $out_tokens ) ) {
101 // NOTE: Only estimate when result is actually text. Embedding replies hold a
102 // float vector, image replies hold URLs/arrays, etc. — running estimate_tokens()
103 // on those JSON-encodes the structure and produces huge bogus counts (a 3072-d
104 // Gemini embedding estimated as ~13k tokens for a 50-char input). Anything that
105 // isn't a string has no meaningful "output token" count, so return 0.
106 if ( !is_string( $this->result ) ) {
107 return 0;
108 }
109 $out_tokens = Meow_MWAI_Core::estimate_tokens( $this->result );
110 }
111 return $out_tokens;
112 }
113
114 public function get_price() {
115 // If it's not set return null, but it can be 0
116 if ( !isset( $this->usage['price'] ) ) {
117 return null;
118 }
119 return $this->usage['price'];
120 }
121
122 public function get_usage_accuracy() {
123 return $this->usage['accuracy'] ?? 'none';
124 }
125
126 /**
127 * Returns the metric count for this reply, preferring tokens. Falls back to
128 * images or seconds for non-token-billed providers (legacy Imagen/Replicate
129 * per-image, Whisper, Sora). Equivalent to (and aliased by) get_units().
130 */
131 public function get_tokens() {
132 if ( isset( $this->usage['total_tokens'] ) ) {
133 return $this->usage['total_tokens'];
134 }
135 else if ( isset( $this->usage['images'] ) ) {
136 return $this->usage['images'];
137 }
138 else if ( isset( $this->usage['seconds'] ) ) {
139 return $this->usage['seconds'];
140 }
141 return null;
142 }
143
144 /**
145 * Legacy alias for get_tokens(). Kept indefinitely — third-party code
146 * reachable via the mwai_ai_reply filter may call it.
147 */
148 public function get_units() {
149 return $this->get_tokens();
150 }
151
152 public function get_type() {
153 return $this->type;
154 }
155
156 public function set_reply( $reply ) {
157 $this->result = $reply;
158 $this->results[] = [ $reply ];
159 }
160
161 public function replace( $search, $replace ) {
162 $this->result = str_replace( $search, $replace, $this->result );
163 $this->results = array_map( function ( $result ) use ( $search, $replace ) {
164 return str_replace( $search, $replace, $result );
165 }, $this->results );
166 }
167
168 private function extract_arguments( $funcArgs ) {
169 $finalArgs = [];
170 if ( is_string( $funcArgs ) ) {
171 $arguments = trim( str_replace( "\n", '', $funcArgs ) );
172 if ( substr( $arguments, 0, 1 ) == '{' ) {
173 $arguments = json_decode( $arguments, true );
174 $finalArgs = $arguments;
175 }
176 }
177 else if ( is_array( $funcArgs ) ) {
178 $finalArgs = $funcArgs;
179 }
180 return $finalArgs;
181 }
182
183 /**
184 * Set the choices from OpenAI as the results.
185 * The last (or only) result is set as the result.
186 * @param array $choices ID of the model to use.
187 */
188 public function set_choices( $choices, $rawMessage = null ) {
189 $this->results = [];
190
191 // Initialize feedback arrays at the start to accumulate across all choices
192 // This is important for engines like Google that split multiple function calls
193 // into separate choices
194 $this->needFeedbacks = [];
195 $this->needClientActions = [];
196
197 if ( is_array( $choices ) ) {
198 foreach ( $choices as $choice ) {
199
200 // It's chat completion
201 if ( isset( $choice['message'] ) ) {
202
203 // It's text content
204 if ( isset( $choice['message']['content'] ) ) {
205 $content = trim( $choice['message']['content'] );
206 $this->results[] = $content;
207 $this->result = $content;
208 }
209
210 // It's a tool call (OpenAI-style and Anthropic-style)
211 $toolCalls = [];
212 if ( isset( $choice['message']['tool_calls'] ) ) {
213 $tools = $choice['message']['tool_calls'];
214 foreach ( $tools as $tool ) {
215 if ( $tool['type'] === 'function' ) {
216 $toolCall = [
217 'toolId' => $tool['id'],
218 //'mode' => 'interactive',
219 'type' => 'tool_call',
220 'name' => trim( $tool['function']['name'] ),
221 'arguments' => $this->extract_arguments( $tool['function']['arguments'] ),
222 // Represent the original message that triggered the function call
223 'rawMessage' => $rawMessage ? $rawMessage : ( isset( $choice['_rawMessage'] ) ? $choice['_rawMessage'] : $choice['message'] ),
224 ];
225 $toolCalls[] = $toolCall;
226 }
227 }
228 }
229
230 // If it's a function call (Open-AI style; usually for a final execution)
231 if ( isset( $choice['message']['function_call'] ) ) {
232 $content = $choice['message']['function_call'];
233 $name = trim( $content['name'] );
234 $args = $content['arguments'] ?? $content['args'] ?? null;
235 $toolCalls[] = [
236 'toolId' => null,
237 'mode' => 'static',
238 'type' => 'function_call',
239 'name' => $name,
240 'arguments' => $this->extract_arguments( $args ),
241 'rawMessage' => $rawMessage ? $rawMessage : ( isset( $choice['_rawMessage'] ) ? $choice['_rawMessage'] : $choice['message'] ),
242 ];
243 }
244
245 // Deep copy tool calls BEFORE adding function references
246 // This prevents the "Duplicate value for 'tool_call_id'" error
247 // when the same function is called multiple times
248 // Note: We need to preserve the toolId for each tool call
249 if ( !empty( $toolCalls ) ) {
250 $toolCalls = json_decode( json_encode( $toolCalls ), true );
251 }
252
253 // Resolve the original function from the query
254 if ( !empty( $toolCalls ) ) {
255 foreach ( $toolCalls as &$toolCall ) {
256 if ( $toolCall['type'] !== 'function_call' && $toolCall['type'] !== 'tool_call' ) {
257 continue;
258 }
259 foreach ( $this->query->functions as $function ) {
260 if ( $function->name == $toolCall['name'] ) {
261 $toolCall['function'] = $function;
262 break;
263 }
264 }
265 }
266 // IMPORTANT: Unset the reference to avoid PHP's foreach reference bug
267 unset( $toolCall );
268 }
269
270 // Add tool calls to existing arrays instead of resetting them
271 // This is crucial for engines like Google that create multiple choices
272 // for multiple function calls in a single response
273 foreach ( $toolCalls as $tcIdx => $toolCall ) {
274 if ( $toolCall['function']->target !== 'js' ) {
275 $this->needFeedbacks[] = $toolCall;
276 }
277 else if ( $toolCall['function']->target === 'js' ) {
278 $this->needClientActions[] = $toolCall;
279 }
280 }
281 }
282
283 // It's text completion
284 else if ( isset( $choice['text'] ) ) {
285
286 // TODO: Assistants return an array (so actually not really a text completion)
287 // We should probably make this clearer and analyze all the outputs from different endpoints.
288 if ( is_array( $choice['text'] ) ) {
289 $text = trim( $choice['text']['value'] );
290 $this->results[] = $text;
291 $this->result = $text;
292 }
293 else {
294 $text = trim( $choice['text'] );
295 $this->results[] = $text;
296 $this->result = $text;
297 }
298 }
299
300 // It's url/image
301 else if ( isset( $choice['url'] ) ) {
302 $url = trim( $choice['url'] );
303 $this->results[] = $url;
304 $this->result = $url;
305 }
306 else if ( isset( $choice['b64_json'] ) ) {
307 // In that case we need to create a temporary file in WordPress to store the image, and return the URL for it.
308 global $mwai_core;
309
310 // Check if the query has explicitly disabled local download
311 if ( !empty( $this->query ) && $this->query instanceof Meow_MWAI_Query_Image && $this->query->localDownload === null ) {
312 // Query explicitly doesn't want local download, save as temporary upload
313 $localDownload = 'uploads';
314 $expiry = 1 * HOUR_IN_SECONDS; // 1 hour for temporary images
315 }
316 else {
317 // Use the user's AI-generated image settings
318 $localDownload = $mwai_core->get_option( 'image_local_download' );
319 $expiry = (int) $mwai_core->get_option( 'image_expires_download' );
320 }
321
322 // The expiry is already in seconds
323 $ttl = $expiry;
324
325 // Use 'library' or 'uploads' based on user settings
326 $target = ( $localDownload === 'library' ) ? 'library' : 'uploads';
327
328 // Prepare metadata similar to regular image queries
329 $metadata = [];
330 if ( !empty( $this->query ) ) {
331 $metadata['query_envId'] = $this->query->envId ?? null;
332 $metadata['query_session'] = $this->query->session ?? null;
333 $metadata['query_model'] = $this->query->model ?? 'gpt-image-1';
334 }
335
336 $url = $mwai_core->files->save_temp_image_from_b64( $choice['b64_json'], 'generated', $ttl, $target, $metadata );
337 if ( is_wp_error( $url ) ) {
338 return $url;
339 }
340 $this->results[] = $url;
341
342 // For chatbot display, append image markdown to the result
343 if ( !empty( $this->result ) ) {
344 $this->result .= "\n\n";
345 }
346 $this->result .= "![Generated Image]($url)";
347 }
348
349 // It's embedding
350 else if ( isset( $choice['embedding'] ) ) {
351 $content = $choice['embedding'];
352 $this->results[] = $content;
353 $this->result = $content;
354 }
355 }
356 }
357 else {
358 $this->result = $choices;
359 $this->results[] = $choices;
360 }
361 }
362
363 public function toJson() {
364 return json_encode( $this );
365 }
366 }
367