| Server IP : 121.121.20.254 / Your IP : 216.73.216.202 Web Server : Microsoft-IIS/10.0 System : Windows NT WEB-SERVER 10.0 build 20348 (Windows Server 2022) AMD64 User : IUSR ( 0) PHP Version : 8.3.28 Disable Function : NONE MySQL : ON | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : C:/inetpub/wwwroot/AIWEBSTATION/wp-content/plugins/ai-engine-pro/classes/ |
Upload File : |
<?php
class Meow_MWAI_Reply implements JsonSerializable {
public $id = null;
public $result = '';
public $results = [];
public $usage = [
'prompt_tokens' => 0,
'completion_tokens' => 0,
'total_tokens' => 0,
'price' => null,
'accuracy' => 'none', // 'none', 'estimated', 'tokens', 'price', 'full'
];
public $query = null;
public $type = 'text';
public $model = null; // Actual model used by the API (may differ from query model)
// Code interpreter code (separate from main content)
public $contentCode = '';
// Engine-specific extras kept on the reply during a single request (e.g. Google
// stores generated images, thoughts and grounding metadata here). Declared so PHP
// 8.2+ does not warn about dynamic property creation.
public $extraData = [];
// This is when models return a message that needs to be executed (functions, tools, etc)
public $needFeedbacks = [];
public $needClientActions = [];
// Tool/function calls actually executed during this turn, oldest first. Accumulated
// across the recursive feedback rounds (see Meow_MWAI_Engines_Core::run) so the final
// reply carries the whole chain, then stored in the discussion for the admin UI.
// Not exposed in jsonSerialize on purpose: arguments and results stay server-side.
public $toolCalls = [];
// What is KEPT for the discussion, not what is sent to the model: the request itself is
// untouched. These are copies written into mwai_chats.messages for the admin UI, so an
// MCP tool returning hundreds of KB, or a wp_write_blocks call carrying a whole article,
// would otherwise be duplicated into the discussion row and stay there forever.
public const TOOL_CALL_RESULT_LIMIT = 500;
// Long argument values are cut individually rather than by chopping the encoded blob:
// the panel shows the parameter names and pretty-prints the JSON, so the structure has
// to survive. Keys and scalars stay intact; only long strings shrink.
public const TOOL_CALL_ARG_VALUE_LIMIT = 200;
// Backstop for arguments made of many small values rather than one big one.
public const TOOL_CALL_ARGS_TOTAL_LIMIT = 4000;
public function __construct( $query = null ) {
$this->query = $query;
}
// Cut a string on a character boundary, so a truncated UTF-8 value never ends in a
// broken sequence that would make the stored JSON unreadable.
private static function truncate_text( $text, $limit ) {
if ( function_exists( 'mb_substr' ) ) {
if ( mb_strlen( $text, 'UTF-8' ) <= $limit ) {
return $text;
}
return mb_substr( $text, 0, $limit, 'UTF-8' );
}
if ( strlen( $text ) <= $limit ) {
return $text;
}
return substr( $text, 0, $limit );
}
private static function shrink_tool_arguments( $value, $depth = 0 ) {
if ( $depth > 6 ) {
return '[…]';
}
if ( is_array( $value ) ) {
$out = [];
foreach ( $value as $key => $item ) {
$out[$key] = self::shrink_tool_arguments( $item, $depth + 1 );
}
return $out;
}
if ( is_string( $value ) && strlen( $value ) > self::TOOL_CALL_ARG_VALUE_LIMIT ) {
// Say how much was dropped, so the panel never looks like the AI sent a short value.
return self::truncate_text( $value, self::TOOL_CALL_ARG_VALUE_LIMIT )
. '… [truncated, ' . size_format( strlen( $value ) ) . ']';
}
return $value;
}
public function add_tool_call( $name, $arguments = null, $result = null, $success = true ) {
$preview = is_string( $result ) ? $result : json_encode( $result );
if ( !is_string( $preview ) ) {
$preview = '';
}
if ( strlen( $preview ) > self::TOOL_CALL_RESULT_LIMIT ) {
$preview = self::truncate_text( $preview, self::TOOL_CALL_RESULT_LIMIT ) . '...';
}
$arguments = is_array( $arguments ) ? self::shrink_tool_arguments( $arguments ) : [];
// Many small values can still add up; fall back to the parameter names alone. Only
// the first few, because a call with hundreds of keys would otherwise produce a
// "summary" larger than the payload it replaces.
$encoded = json_encode( $arguments );
if ( is_string( $encoded ) && strlen( $encoded ) > self::TOOL_CALL_ARGS_TOTAL_LIMIT ) {
$keys = array_keys( $arguments );
$shown = array_slice( $keys, 0, 20 );
$summary = implode( ', ', $shown );
if ( count( $keys ) > count( $shown ) ) {
$summary .= ', and ' . ( count( $keys ) - count( $shown ) ) . ' more';
}
$arguments = [ '_truncated' => 'Arguments too large to store. Parameters: ' . $summary ];
}
$this->toolCalls[] = [
'name' => (string) $name,
'arguments' => $arguments,
'result' => $preview,
'success' => (bool) $success,
];
}
#[\ReturnTypeWillChange]
public function jsonSerialize() {
$isEmbedding = false;
$embeddingsDimensions = null;
$embedddingsMessage = null;
if ( is_array( $this->results ) && count( $this->results ) > 0 ) {
$isEmbedding = is_array( $this->results[0] );
if ( $isEmbedding ) {
$embeddingsDimensions = count( $this->results[0] );
$embedddingsMessage = "A $embeddingsDimensions-dimensional embedding was returned.";
}
}
$data = [
'result' => $isEmbedding ? $embedddingsMessage : $this->result,
'results' => $isEmbedding ? [] : $this->results,
'usage' => $this->usage,
'system' => [
'class' => get_class( $this ),
]
];
if ( !empty( $this->needFeedbacks ) ) {
$data['needFeedbacks'] = $this->needFeedbacks;
}
if ( !empty( $this->needClientActions ) ) {
$data['needClientActions'] = $this->needClientActions;
}
if ( !empty( $this->contentCode ) ) {
$data['contentCode'] = $this->contentCode;
}
return $data;
}
public function set_usage( $usage ) {
$this->usage = $usage;
}
public function set_usage_accuracy( $accuracy ) {
$this->usage['accuracy'] = $accuracy;
}
public function set_id( $id ) {
$this->id = $id;
}
public function set_type( $type ) {
$this->type = $type;
}
public function set_model( $model ) {
$this->model = $model;
}
public function get_total_tokens() {
return isset( $this->usage['total_tokens'] ) ? $this->usage['total_tokens'] : 0;
}
public function get_in_tokens( $query = null ) {
$in_tokens = isset( $this->usage['prompt_tokens'] ) ? $this->usage['prompt_tokens'] : 0;
if ( empty( $in_tokens ) && $query ) {
$in_tokens = $query->get_in_tokens();
}
return $in_tokens;
}
public function get_out_tokens() {
$out_tokens = isset( $this->usage['completion_tokens'] ) ? $this->usage['completion_tokens'] : 0;
if ( empty( $out_tokens ) ) {
// NOTE: Only estimate when result is actually text. Embedding replies hold a
// float vector, image replies hold URLs/arrays, etc. — running estimate_tokens()
// on those JSON-encodes the structure and produces huge bogus counts (a 3072-d
// Gemini embedding estimated as ~13k tokens for a 50-char input). Anything that
// isn't a string has no meaningful "output token" count, so return 0.
if ( !is_string( $this->result ) ) {
return 0;
}
$out_tokens = Meow_MWAI_Core::estimate_tokens( $this->result );
}
return $out_tokens;
}
public function get_price() {
// If it's not set return null, but it can be 0
if ( !isset( $this->usage['price'] ) ) {
return null;
}
return $this->usage['price'];
}
public function get_usage_accuracy() {
return $this->usage['accuracy'] ?? 'none';
}
/**
* Returns the metric count for this reply, preferring tokens. Falls back to
* images or seconds for non-token-billed providers (legacy Imagen/Replicate
* per-image, Whisper, Sora). Equivalent to (and aliased by) get_units().
*/
public function get_tokens() {
if ( isset( $this->usage['total_tokens'] ) ) {
return $this->usage['total_tokens'];
}
else if ( isset( $this->usage['images'] ) ) {
return $this->usage['images'];
}
else if ( isset( $this->usage['seconds'] ) ) {
return $this->usage['seconds'];
}
return null;
}
/**
* Legacy alias for get_tokens(). Kept indefinitely — third-party code
* reachable via the mwai_ai_reply filter may call it.
*/
public function get_units() {
return $this->get_tokens();
}
public function get_type() {
return $this->type;
}
public function set_reply( $reply ) {
$this->result = $reply;
$this->results[] = [ $reply ];
}
public function replace( $search, $replace ) {
$this->result = str_replace( $search, $replace, $this->result );
$this->results = array_map( function ( $result ) use ( $search, $replace ) {
return str_replace( $search, $replace, $result );
}, $this->results );
}
private function extract_arguments( $funcArgs ) {
$finalArgs = [];
if ( is_string( $funcArgs ) ) {
$arguments = trim( str_replace( "\n", '', $funcArgs ) );
if ( substr( $arguments, 0, 1 ) == '{' ) {
$arguments = json_decode( $arguments, true );
$finalArgs = $arguments;
}
}
else if ( is_array( $funcArgs ) ) {
$finalArgs = $funcArgs;
}
return $finalArgs;
}
/**
* Set the choices from OpenAI as the results.
* The last (or only) result is set as the result.
* @param array $choices ID of the model to use.
*/
public function set_choices( $choices, $rawMessage = null ) {
$this->results = [];
// Initialize feedback arrays at the start to accumulate across all choices
// This is important for engines like Google that split multiple function calls
// into separate choices
$this->needFeedbacks = [];
$this->needClientActions = [];
if ( is_array( $choices ) ) {
foreach ( $choices as $choice ) {
// It's chat completion
if ( isset( $choice['message'] ) ) {
// It's text content
if ( isset( $choice['message']['content'] ) ) {
$content = trim( $choice['message']['content'] );
$this->results[] = $content;
$this->result = $content;
}
// It's a tool call (OpenAI-style and Anthropic-style)
$toolCalls = [];
if ( isset( $choice['message']['tool_calls'] ) ) {
$tools = $choice['message']['tool_calls'];
foreach ( $tools as $tool ) {
if ( $tool['type'] === 'function' ) {
$toolCall = [
'toolId' => $tool['id'],
//'mode' => 'interactive',
'type' => 'tool_call',
'name' => trim( $tool['function']['name'] ),
'arguments' => $this->extract_arguments( $tool['function']['arguments'] ),
// Represent the original message that triggered the function call
'rawMessage' => $rawMessage ? $rawMessage : ( isset( $choice['_rawMessage'] ) ? $choice['_rawMessage'] : $choice['message'] ),
];
$toolCalls[] = $toolCall;
}
}
}
// If it's a function call (Open-AI style; usually for a final execution)
if ( isset( $choice['message']['function_call'] ) ) {
$content = $choice['message']['function_call'];
$name = trim( $content['name'] );
$args = $content['arguments'] ?? $content['args'] ?? null;
$toolCalls[] = [
'toolId' => null,
'mode' => 'static',
'type' => 'function_call',
'name' => $name,
'arguments' => $this->extract_arguments( $args ),
'rawMessage' => $rawMessage ? $rawMessage : ( isset( $choice['_rawMessage'] ) ? $choice['_rawMessage'] : $choice['message'] ),
];
}
// Deep copy tool calls BEFORE adding function references
// This prevents the "Duplicate value for 'tool_call_id'" error
// when the same function is called multiple times
// Note: We need to preserve the toolId for each tool call
if ( !empty( $toolCalls ) ) {
$toolCalls = json_decode( json_encode( $toolCalls ), true );
}
// Resolve the original function from the query
if ( !empty( $toolCalls ) ) {
foreach ( $toolCalls as &$toolCall ) {
if ( $toolCall['type'] !== 'function_call' && $toolCall['type'] !== 'tool_call' ) {
continue;
}
foreach ( $this->query->functions as $function ) {
if ( $function->name == $toolCall['name'] ) {
$toolCall['function'] = $function;
break;
}
}
}
// IMPORTANT: Unset the reference to avoid PHP's foreach reference bug
unset( $toolCall );
}
// Add tool calls to existing arrays instead of resetting them
// This is crucial for engines like Google that create multiple choices
// for multiple function calls in a single response
foreach ( $toolCalls as $tcIdx => $toolCall ) {
if ( $toolCall['function']->target !== 'js' ) {
$this->needFeedbacks[] = $toolCall;
}
else if ( $toolCall['function']->target === 'js' ) {
$this->needClientActions[] = $toolCall;
}
}
}
// It's text completion
else if ( isset( $choice['text'] ) ) {
// TODO: Assistants return an array (so actually not really a text completion)
// We should probably make this clearer and analyze all the outputs from different endpoints.
if ( is_array( $choice['text'] ) ) {
$text = trim( $choice['text']['value'] );
$this->results[] = $text;
$this->result = $text;
}
else {
$text = trim( $choice['text'] );
$this->results[] = $text;
$this->result = $text;
}
}
// It's url/image
else if ( isset( $choice['url'] ) ) {
$url = trim( $choice['url'] );
$this->results[] = $url;
$this->result = $url;
}
else if ( isset( $choice['b64_json'] ) ) {
// In that case we need to create a temporary file in WordPress to store the image, and return the URL for it.
global $mwai_core;
// Check if the query has explicitly disabled local download
if ( !empty( $this->query ) && $this->query instanceof Meow_MWAI_Query_Image && $this->query->localDownload === null ) {
// Query explicitly doesn't want local download, save as temporary upload
$localDownload = 'uploads';
$expiry = 1 * HOUR_IN_SECONDS; // 1 hour for temporary images
}
else {
// Use the user's AI-generated image settings. The Expiration
// setting displays "Never" as its default, so an unset option must
// mean never; letting it fall through to the uploaded-files TTL
// (1 hour) silently rotted images out of stored conversations.
$localDownload = $mwai_core->get_option( 'image_local_download' );
$expiryOption = $mwai_core->get_option( 'image_expires_download' );
$expiry = ( empty( $expiryOption ) || strtolower( (string) $expiryOption ) === 'never' )
? 'never' : (int) $expiryOption;
}
// The expiry is in seconds, or 'never'.
$ttl = $expiry;
// Use 'library' or 'uploads' based on user settings
$target = ( $localDownload === 'library' ) ? 'library' : 'uploads';
// Prepare metadata similar to regular image queries
$metadata = [];
if ( !empty( $this->query ) ) {
$metadata['query_envId'] = $this->query->envId ?? null;
$metadata['query_session'] = $this->query->session ?? null;
$metadata['query_model'] = $this->query->model ?? 'gpt-image-1';
}
$url = $mwai_core->files->save_temp_image_from_b64( $choice['b64_json'], 'generated', $ttl, $target, $metadata );
if ( is_wp_error( $url ) ) {
return $url;
}
$this->results[] = $url;
// For chatbot display, append image markdown to the result
if ( !empty( $this->result ) ) {
$this->result .= "\n\n";
}
$this->result .= "";
}
// It's embedding
else if ( isset( $choice['embedding'] ) ) {
$content = $choice['embedding'];
$this->results[] = $content;
$this->result = $content;
}
}
}
else {
$this->result = $choices;
$this->results[] = $choices;
}
}
public function toJson() {
return json_encode( $this );
}
}