diff --git a/src/AiCommand.php b/src/AiCommand.php index 91e4f7c..3f0cc1f 100644 --- a/src/AiCommand.php +++ b/src/AiCommand.php @@ -45,11 +45,11 @@ class AiCommand extends WP_CLI_Command { */ public function __invoke( $args, $assoc_args ) { $server = new MCP\Server(); - $client = new MCP\Client($server); + $client = new MCP\Client( $server ); - $this->register_tools($server, $client); + $this->register_tools( $server, $client ); - $this->register_resources($server); + $this->register_resources( $server ); $result = $client->call_ai_service_with_prompt( $args[0] ); @@ -57,7 +57,7 @@ public function __invoke( $args, $assoc_args ) { } // Register tools for AI processing - private function register_tools($server, $client) { + private function register_tools( $server, $client ) { $server->register_tool( [ 'name' => 'calculate_total', @@ -105,135 +105,141 @@ private function register_tools($server, $client) { ] ); +// $server->register_tool( +// [ +// 'name' => 'generate_image', +// 'description' => 'Generates an image.', +// 'inputSchema' => [ +// 'type' => 'object', +// 'properties' => [ +// 'prompt' => [ +// 'type' => 'string', +// 'description' => 'The prompt for generating the image.', +// ], +// ], +// 'required' => [ 'prompt' ], +// ], +// 'callable' => function ( $params ) use ( $client ) { +// return $client->get_image_from_ai_service( $params['prompt'] ); +// }, +// ] +// ); + $server->register_tool( [ - 'name' => 'generate_image', - 'description' => 'Generates an image.', + 'name' => 'fetch_wp_community_events', + 'description' => 'Fetches upcoming WordPress community events near a specified city or the user\'s current location. If no events are found in the exact location, nearby events within a specific radius will be considered.', 'inputSchema' => [ 'type' => 'object', 'properties' => [ - 'prompt' => [ + 'location' => [ 'type' => 'string', - 'description' => 'The prompt for generating the image.', + 'description' => 'City name or "near me" for auto-detected location. If no events are found in the exact location, the tool will also consider nearby events within a specified radius (default: 100 km).', ], ], - 'required' => [ 'prompt' ], + 'required' => [ 'location' ], // We only require the location ], - 'callable' => function ( $params ) use ( $client ) { - return $client->get_image_from_ai_service( $params['prompt'] ); - }, - ] - ); + 'callable' => function ( $params ) { + // Default user ID is 0 + $user_id = 0; + + // Get the location from the parameters (already supplied in the prompt) + $location_input = strtolower( trim( $params['location'] ) ); + + // Manually include the WP_Community_Events class if it's not loaded + if ( ! class_exists( 'WP_Community_Events' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php'; + } + + // Determine location for the WP_Community_Events class + $location = null; + if ( $location_input !== 'near me' ) { + // Provide city name (WP will resolve coordinates) + $location = [ + 'description' => $location_input, + ]; + } - $server->register_tool( - [ - 'name' => 'fetch_wp_community_events', - 'description' => 'Fetches upcoming WordPress community events near a specified city or the user\'s current location. If no events are found in the exact location, nearby events within a specific radius will be considered.', - 'inputSchema' => [ - 'type' => 'object', - 'properties' => [ - 'location' => [ - 'type' => 'string', - 'description' => 'City name or "near me" for auto-detected location. If no events are found in the exact location, the tool will also consider nearby events within a specified radius (default: 100 km).', - ], - ], - 'required' => [ 'location' ], // We only require the location - ], - 'callable' => function ( $params ) { - // Default user ID is 0 - $user_id = 0; - - // Get the location from the parameters (already supplied in the prompt) - $location_input = strtolower( trim( $params['location'] ) ); - - // Manually include the WP_Community_Events class if it's not loaded - if ( ! class_exists( 'WP_Community_Events' ) ) { - require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php'; - } - - // Determine location for the WP_Community_Events class - $location = null; - if ( $location_input !== 'near me' ) { - // Provide city name (WP will resolve coordinates) - $location = [ - 'description' => $location_input, - ]; - } - - // Instantiate WP_Community_Events with user ID (0) and optional location - $events_instance = new WP_Community_Events( $user_id, $location ); - - // Get events from WP_Community_Events - $events = $events_instance->get_events($location_input); - - // Check for WP_Error - if ( is_wp_error( $events ) ) { - return [ 'error' => $events->get_error_message() ]; - } + // Instantiate WP_Community_Events with user ID (0) and optional location + $events_instance = new WP_Community_Events( $user_id, $location ); + + // Get events from WP_Community_Events + $events = $events_instance->get_events( $location_input ); + + // Check for WP_Error + if ( is_wp_error( $events ) ) { + return [ 'error' => $events->get_error_message() ]; + } // If no events found - if ( empty( $events['events'] ) ) { - return [ 'message' => 'No events found near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) ]; - } + if ( empty( $events['events'] ) ) { + return [ 'message' => 'No events found near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) ]; + } // Format and return the events correctly - $formatted_events = array_map( function ( $event ) { - // Log event details to ensure properties are accessible - error_log( 'Event details: ' . print_r( $event, true ) ); + $formatted_events = array_map( + function ( $event ) { + // Log event details to ensure properties are accessible + error_log( 'Event details: ' . print_r( $event, true ) ); - // Initialize a formatted event string - $formatted_event = ''; + // Initialize a formatted event string + $formatted_event = ''; - // Format event title - if ( isset( $event['title'] ) ) { + // Format event title + if ( isset( $event['title'] ) ) { $formatted_event .= $event['title'] . "\n"; - } + } - // Format the date nicely - $formatted_event .= ' - Date: ' . ( isset( $event['date'] ) ? date( 'F j, Y g:i A', strtotime( $event['date'] ) ) : 'No date available' ) . "\n"; + // Format the date nicely + $formatted_event .= ' - Date: ' . ( isset( $event['date'] ) ? date( 'F j, Y g:i A', strtotime( $event['date'] ) ) : 'No date available' ) . "\n"; - // Format the location - if ( isset( $event['location']['location'] ) ) { - $formatted_event .= ' - Location: ' . $event['location']['location'] . "\n"; - } + // Format the location + if ( isset( $event['location']['location'] ) ) { + $formatted_event .= ' - Location: ' . $event['location']['location'] . "\n"; + } - // Format the event URL - $formatted_event .= isset( $event['url'] ) ? ' - URL: ' . $event['url'] . "\n" : ''; + // Format the event URL + $formatted_event .= isset( $event['url'] ) ? ' - URL: ' . $event['url'] . "\n" : ''; - return $formatted_event; - }, $events['events'] ); + return $formatted_event; + }, + $events['events'] + ); // Combine the formatted events into a single string - $formatted_events_output = implode("\n", $formatted_events); + $formatted_events_output = implode( "\n", $formatted_events ); // Return the formatted events string return [ - 'message' => "OK. I found " . count($formatted_events) . " WordPress events near " . ( $location_input === 'near me' ? 'your location' : $location_input ) . ":\n\n" . $formatted_events_output + 'message' => 'OK. I found ' . count( $formatted_events ) . ' WordPress events near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) . ":\n\n" . $formatted_events_output, ]; - }, + }, ] ); - } // Register resources for AI access - private function register_resources($server) { + private function register_resources( $server ) { // Register Users resource - $server->register_resource([ + $server->register_resource( + [ 'name' => 'users', 'uri' => 'data://users', 'description' => 'List of users', 'mimeType' => 'application/json', 'dataKey' => 'users', // Data will be fetched from 'users' - ]); + ] + ); // Register Product Catalog resource - $server->register_resource([ + $server->register_resource( + [ 'name' => 'product_catalog', 'uri' => 'file://./products.json', 'description' => 'Product catalog', 'mimeType' => 'application/json', 'filePath' => './products.json', // Data will be fetched from products.json - ]); + ] + ); } } diff --git a/src/MCP/Client.php b/src/MCP/Client.php index 283f683..03ce298 100644 --- a/src/MCP/Client.php +++ b/src/MCP/Client.php @@ -6,12 +6,14 @@ use Felix_Arntz\AI_Services\Services\API\Enums\AI_Capability; use Felix_Arntz\AI_Services\Services\API\Enums\Content_Role; use Felix_Arntz\AI_Services\Services\API\Helpers; +use Felix_Arntz\AI_Services\Services\API\Types\Blob; use Felix_Arntz\AI_Services\Services\API\Types\Content; use Felix_Arntz\AI_Services\Services\API\Types\Parts; use Felix_Arntz\AI_Services\Services\API\Types\Parts\File_Data_Part; use Felix_Arntz\AI_Services\Services\API\Types\Parts\Function_Call_Part; use Felix_Arntz\AI_Services\Services\API\Types\Parts\Inline_Data_Part; use Felix_Arntz\AI_Services\Services\API\Types\Parts\Text_Part; +use Felix_Arntz\AI_Services\Services\API\Types\Text_Generation_Config; use Felix_Arntz\AI_Services\Services\API\Types\Tools; use WP_CLI; @@ -130,9 +132,20 @@ static function () { public function call_ai_service_with_prompt( string $prompt ) { $parts = new Parts(); $parts->add_text_part( $prompt ); - $content = new Content( Content_Role::USER, $parts ); - return $this->call_ai_service( [ $content ] ); + $contents = [ + new Content( Content_Role::USER, $parts ), + ]; + +// $parts = new Parts(); +// $parts->add_inline_data_part( +// 'image/png', +// Helpers::blob_to_base64_data_url( new Blob( file_get_contents( '/private/tmp/ai-generated-imaget1sjmomi30i31C1YtZy.png' ), 'image/png' ) ), +// ); +// +// $contents[] = $parts; + + return $this->call_ai_service( $contents ); } private function call_ai_service( $contents ) { @@ -172,25 +185,33 @@ static function () { ] ); - \WP_CLI::debug( 'Making request...' . print_r( $contents, true ), 'ai' ); + \WP_CLI::debug( 'Making request...' . print_r( $contents, true ), 'ai' ); if ( $service->get_service_slug() === 'openai' ) { $model = 'gpt-4o'; } else { - $model = 'gemini-2.0-flash'; + $model = 'gemini-2.0-flash-exp'; } $candidates = $service ->get_model( [ - 'feature' => 'text-generation', - 'model' => $model, - 'tools' => $tools, - 'capabilities' => [ - AI_Capability::MULTIMODAL_INPUT, - AI_Capability::TEXT_GENERATION, - AI_Capability::FUNCTION_CALLING, - ], + 'feature' => 'text-generation', + 'model' => $model, + // 'tools' => $tools, + 'capabilities' => [ + AI_Capability::MULTIMODAL_INPUT, + AI_Capability::TEXT_GENERATION, + // AI_Capability::FUNCTION_CALLING, + ], + 'generationConfig' => Text_Generation_Config::from_array( + array( + 'responseModalities' => array( + 'Text', + 'Image', + ), + ) + ), ] ) ->generate_text( $contents ); @@ -224,6 +245,46 @@ static function () { $parts->add_function_response_part( $part->get_id(), $part->get_name(), $function_result ); $content = new Content( Content_Role::USER, $parts ); $new_contents[] = $content; + } elseif ( $part instanceof Inline_Data_Part ) { + $image_url = $part->get_base64_data(); // Data URL. + $image_blob = Helpers::base64_data_url_to_blob( $image_url ); + + if ( $image_blob ) { + $filename = tempnam( '/tmp', 'ai-generated-image' ); + $parts = explode( '/', $part->get_mime_type() ); + $extension = $parts[1]; + rename( $filename, $filename . '.' . $extension ); + $filename .= '.' . $extension; + + file_put_contents( $filename, $image_blob->get_binary_data() ); + + $image_url = $filename; + } else { + $binary_data = base64_decode( $image_url ); + if ( false !== $binary_data ) { + $image_blob = new Blob( $binary_data, $part->get_mime_type() ); + + $filename = tempnam( '/tmp', 'ai-generated-image' ); + $parts = explode( '/', $part->get_mime_type() ); + $extension = $parts[1]; + rename( $filename, $filename . '.' . $extension ); + $filename .= '.' . $extension; + + file_put_contents( $filename, $image_blob->get_binary_data() ); + + $image_url = $filename; + } + } + + $text .= "Generated image: $image_url\n"; + + break; + } + + if ( $part instanceof File_Data_Part ) { + $image_url = $part->get_file_uri(); // Actual URL. May have limited TTL (often 1 hour). + // TODO: Save as file or so. + break; } }