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