Skip to content

Commit 5862436

Browse files
authored
Image generation/editing using Gemini 2.0 Flash (#12)
* Formatting fixes * Gemini 2.0 flash * POC to pass an existing image to Gemini
1 parent 10f2e19 commit 5862436

File tree

2 files changed

+168
-100
lines changed

2 files changed

+168
-100
lines changed

src/AiCommand.php

Lines changed: 95 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,19 @@ class AiCommand extends WP_CLI_Command {
4646
*/
4747
public function __invoke( $args, $assoc_args ) {
4848
$server = new MCP\Server();
49-
$client = new MCP\Client($server);
49+
$client = new MCP\Client( $server );
5050

51-
$this->register_tools($server, $client);
51+
$this->register_tools( $server, $client );
5252

53-
$this->register_resources($server);
53+
$this->register_resources( $server );
5454

5555
$result = $client->call_ai_service_with_prompt( $args[0] );
5656

5757
WP_CLI::success( $result );
5858
}
5959

6060
// Register tools for AI processing
61-
private function register_tools($server, $client) {
61+
private function register_tools( $server, $client ) {
6262
$server->register_tool(
6363
[
6464
'name' => 'list_tools',
@@ -206,134 +206,141 @@ private function register_tools($server, $client) {
206206
]
207207
);
208208

209+
// $server->register_tool(
210+
// [
211+
// 'name' => 'generate_image',
212+
// 'description' => 'Generates an image.',
213+
// 'inputSchema' => [
214+
// 'type' => 'object',
215+
// 'properties' => [
216+
// 'prompt' => [
217+
// 'type' => 'string',
218+
// 'description' => 'The prompt for generating the image.',
219+
// ],
220+
// ],
221+
// 'required' => [ 'prompt' ],
222+
// ],
223+
// 'callable' => function ( $params ) use ( $client ) {
224+
// return $client->get_image_from_ai_service( $params['prompt'] );
225+
// },
226+
// ]
227+
// );
228+
209229
$server->register_tool(
210230
[
211-
'name' => 'generate_image',
212-
'description' => 'Generates an image.',
231+
'name' => 'fetch_wp_community_events',
232+
'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.',
213233
'inputSchema' => [
214234
'type' => 'object',
215235
'properties' => [
216-
'prompt' => [
236+
'location' => [
217237
'type' => 'string',
218-
'description' => 'The prompt for generating the image.',
238+
'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).',
219239
],
220240
],
221-
'required' => [ 'prompt' ],
241+
'required' => [ 'location' ], // We only require the location
222242
],
223-
'callable' => function ( $params ) use ( $client ) {
224-
return $client->get_image_from_ai_service( $params['prompt'] );
225-
},
226-
]
227-
);
243+
'callable' => function ( $params ) {
244+
// Default user ID is 0
245+
$user_id = 0;
246+
247+
// Get the location from the parameters (already supplied in the prompt)
248+
$location_input = strtolower( trim( $params['location'] ) );
249+
250+
// Manually include the WP_Community_Events class if it's not loaded
251+
if ( ! class_exists( 'WP_Community_Events' ) ) {
252+
require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php';
253+
}
254+
255+
// Determine location for the WP_Community_Events class
256+
$location = null;
257+
if ( $location_input !== 'near me' ) {
258+
// Provide city name (WP will resolve coordinates)
259+
$location = [
260+
'description' => $location_input,
261+
];
262+
}
228263

229-
$server->register_tool(
230-
[
231-
'name' => 'fetch_wp_community_events',
232-
'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.',
233-
'inputSchema' => [
234-
'type' => 'object',
235-
'properties' => [
236-
'location' => [
237-
'type' => 'string',
238-
'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).',
239-
],
240-
],
241-
'required' => [ 'location' ], // We only require the location
242-
],
243-
'callable' => function ( $params ) {
244-
// Default user ID is 0
245-
$user_id = 0;
246-
247-
// Get the location from the parameters (already supplied in the prompt)
248-
$location_input = strtolower( trim( $params['location'] ) );
249-
250-
// Manually include the WP_Community_Events class if it's not loaded
251-
if ( ! class_exists( 'WP_Community_Events' ) ) {
252-
require_once ABSPATH . 'wp-admin/includes/class-wp-community-events.php';
253-
}
254-
255-
// Determine location for the WP_Community_Events class
256-
$location = null;
257-
if ( $location_input !== 'near me' ) {
258-
// Provide city name (WP will resolve coordinates)
259-
$location = [
260-
'description' => $location_input,
261-
];
262-
}
263-
264-
// Instantiate WP_Community_Events with user ID (0) and optional location
265-
$events_instance = new WP_Community_Events( $user_id, $location );
266-
267-
// Get events from WP_Community_Events
268-
$events = $events_instance->get_events($location_input);
269-
270-
// Check for WP_Error
271-
if ( is_wp_error( $events ) ) {
272-
return [ 'error' => $events->get_error_message() ];
273-
}
264+
// Instantiate WP_Community_Events with user ID (0) and optional location
265+
$events_instance = new WP_Community_Events( $user_id, $location );
266+
267+
// Get events from WP_Community_Events
268+
$events = $events_instance->get_events( $location_input );
269+
270+
// Check for WP_Error
271+
if ( is_wp_error( $events ) ) {
272+
return [ 'error' => $events->get_error_message() ];
273+
}
274274

275275
// If no events found
276-
if ( empty( $events['events'] ) ) {
277-
return [ 'message' => 'No events found near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) ];
278-
}
276+
if ( empty( $events['events'] ) ) {
277+
return [ 'message' => 'No events found near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) ];
278+
}
279279

280280
// Format and return the events correctly
281-
$formatted_events = array_map( function ( $event ) {
282-
// Log event details to ensure properties are accessible
283-
error_log( 'Event details: ' . print_r( $event, true ) );
281+
$formatted_events = array_map(
282+
function ( $event ) {
283+
// Log event details to ensure properties are accessible
284+
error_log( 'Event details: ' . print_r( $event, true ) );
284285

285-
// Initialize a formatted event string
286-
$formatted_event = '';
286+
// Initialize a formatted event string
287+
$formatted_event = '';
287288

288-
// Format event title
289-
if ( isset( $event['title'] ) ) {
289+
// Format event title
290+
if ( isset( $event['title'] ) ) {
290291
$formatted_event .= $event['title'] . "\n";
291-
}
292+
}
292293

293-
// Format the date nicely
294-
$formatted_event .= ' - Date: ' . ( isset( $event['date'] ) ? date( 'F j, Y g:i A', strtotime( $event['date'] ) ) : 'No date available' ) . "\n";
294+
// Format the date nicely
295+
$formatted_event .= ' - Date: ' . ( isset( $event['date'] ) ? date( 'F j, Y g:i A', strtotime( $event['date'] ) ) : 'No date available' ) . "\n";
295296

296-
// Format the location
297-
if ( isset( $event['location']['location'] ) ) {
298-
$formatted_event .= ' - Location: ' . $event['location']['location'] . "\n";
299-
}
297+
// Format the location
298+
if ( isset( $event['location']['location'] ) ) {
299+
$formatted_event .= ' - Location: ' . $event['location']['location'] . "\n";
300+
}
300301

301-
// Format the event URL
302-
$formatted_event .= isset( $event['url'] ) ? ' - URL: ' . $event['url'] . "\n" : '';
302+
// Format the event URL
303+
$formatted_event .= isset( $event['url'] ) ? ' - URL: ' . $event['url'] . "\n" : '';
303304

304-
return $formatted_event;
305-
}, $events['events'] );
305+
return $formatted_event;
306+
},
307+
$events['events']
308+
);
306309

307310
// Combine the formatted events into a single string
308-
$formatted_events_output = implode("\n", $formatted_events);
311+
$formatted_events_output = implode( "\n", $formatted_events );
309312

310313
// Return the formatted events string
311314
return [
312-
'message' => "OK. I found " . count($formatted_events) . " WordPress events near " . ( $location_input === 'near me' ? 'your location' : $location_input ) . ":\n\n" . $formatted_events_output
315+
'message' => 'OK. I found ' . count( $formatted_events ) . ' WordPress events near ' . ( $location_input === 'near me' ? 'your location' : $location_input ) . ":\n\n" . $formatted_events_output,
313316
];
314-
},
317+
},
315318
]
316319
);
317320
}
318321

319322
// Register resources for AI access
320-
private function register_resources($server) {
323+
private function register_resources( $server ) {
321324
// Register Users resource
322-
$server->register_resource([
325+
$server->register_resource(
326+
[
323327
'name' => 'users',
324328
'uri' => 'data://users',
325329
'description' => 'List of users',
326330
'mimeType' => 'application/json',
327331
'dataKey' => 'users', // Data will be fetched from 'users'
328-
]);
332+
]
333+
);
329334

330335
// Register Product Catalog resource
331-
$server->register_resource([
336+
$server->register_resource(
337+
[
332338
'name' => 'product_catalog',
333339
'uri' => 'file://./products.json',
334340
'description' => 'Product catalog',
335341
'mimeType' => 'application/json',
336342
'filePath' => './products.json', // Data will be fetched from products.json
337-
]);
343+
]
344+
);
338345
}
339346
}

src/MCP/Client.php

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
use Felix_Arntz\AI_Services\Services\API\Enums\AI_Capability;
77
use Felix_Arntz\AI_Services\Services\API\Enums\Content_Role;
88
use Felix_Arntz\AI_Services\Services\API\Helpers;
9+
use Felix_Arntz\AI_Services\Services\API\Types\Blob;
910
use Felix_Arntz\AI_Services\Services\API\Types\Content;
1011
use Felix_Arntz\AI_Services\Services\API\Types\Parts;
1112
use Felix_Arntz\AI_Services\Services\API\Types\Parts\File_Data_Part;
1213
use Felix_Arntz\AI_Services\Services\API\Types\Parts\Function_Call_Part;
1314
use Felix_Arntz\AI_Services\Services\API\Types\Parts\Inline_Data_Part;
1415
use Felix_Arntz\AI_Services\Services\API\Types\Parts\Text_Part;
16+
use Felix_Arntz\AI_Services\Services\API\Types\Text_Generation_Config;
1517
use Felix_Arntz\AI_Services\Services\API\Types\Tools;
1618
use WP_CLI;
1719

@@ -131,9 +133,20 @@ public function call_ai_service_with_prompt( string $prompt ) {
131133
\WP_CLI::debug( "Prompt: {$prompt}", 'mcp_server' );
132134
$parts = new Parts();
133135
$parts->add_text_part( $prompt );
134-
$content = new Content( Content_Role::USER, $parts );
135136

136-
return $this->call_ai_service( [ $content ] );
137+
$contents = [
138+
new Content( Content_Role::USER, $parts ),
139+
];
140+
141+
// $parts = new Parts();
142+
// $parts->add_inline_data_part(
143+
// 'image/png',
144+
// Helpers::blob_to_base64_data_url( new Blob( file_get_contents( '/private/tmp/ai-generated-imaget1sjmomi30i31C1YtZy.png' ), 'image/png' ) ),
145+
// );
146+
//
147+
// $contents[] = $parts;
148+
149+
return $this->call_ai_service( $contents );
137150
}
138151

139152
private function call_ai_service( $contents ) {
@@ -173,25 +186,33 @@ static function () {
173186
]
174187
);
175188

176-
\WP_CLI::debug( 'Making request...' . print_r( $contents, true ), 'ai' );
189+
\WP_CLI::debug( 'Making request...' . print_r( $contents, true ), 'ai' );
177190

178191
if ( $service->get_service_slug() === 'openai' ) {
179192
$model = 'gpt-4o';
180193
} else {
181-
$model = 'gemini-2.0-flash';
194+
$model = 'gemini-2.0-flash-exp';
182195
}
183196

184197
$candidates = $service
185198
->get_model(
186199
[
187-
'feature' => 'text-generation',
188-
'model' => $model,
189-
'tools' => $tools,
190-
'capabilities' => [
191-
AI_Capability::MULTIMODAL_INPUT,
192-
AI_Capability::TEXT_GENERATION,
193-
AI_Capability::FUNCTION_CALLING,
194-
],
200+
'feature' => 'text-generation',
201+
'model' => $model,
202+
// 'tools' => $tools,
203+
'capabilities' => [
204+
AI_Capability::MULTIMODAL_INPUT,
205+
AI_Capability::TEXT_GENERATION,
206+
// AI_Capability::FUNCTION_CALLING,
207+
],
208+
'generationConfig' => Text_Generation_Config::from_array(
209+
array(
210+
'responseModalities' => array(
211+
'Text',
212+
'Image',
213+
),
214+
)
215+
),
195216
]
196217
)
197218
->generate_text( $contents );
@@ -225,6 +246,46 @@ static function () {
225246
$parts->add_function_response_part( $part->get_id(), $part->get_name(), $function_result );
226247
$content = new Content( Content_Role::USER, $parts );
227248
$new_contents[] = $content;
249+
} elseif ( $part instanceof Inline_Data_Part ) {
250+
$image_url = $part->get_base64_data(); // Data URL.
251+
$image_blob = Helpers::base64_data_url_to_blob( $image_url );
252+
253+
if ( $image_blob ) {
254+
$filename = tempnam( '/tmp', 'ai-generated-image' );
255+
$parts = explode( '/', $part->get_mime_type() );
256+
$extension = $parts[1];
257+
rename( $filename, $filename . '.' . $extension );
258+
$filename .= '.' . $extension;
259+
260+
file_put_contents( $filename, $image_blob->get_binary_data() );
261+
262+
$image_url = $filename;
263+
} else {
264+
$binary_data = base64_decode( $image_url );
265+
if ( false !== $binary_data ) {
266+
$image_blob = new Blob( $binary_data, $part->get_mime_type() );
267+
268+
$filename = tempnam( '/tmp', 'ai-generated-image' );
269+
$parts = explode( '/', $part->get_mime_type() );
270+
$extension = $parts[1];
271+
rename( $filename, $filename . '.' . $extension );
272+
$filename .= '.' . $extension;
273+
274+
file_put_contents( $filename, $image_blob->get_binary_data() );
275+
276+
$image_url = $filename;
277+
}
278+
}
279+
280+
$text .= "Generated image: $image_url\n";
281+
282+
break;
283+
}
284+
285+
if ( $part instanceof File_Data_Part ) {
286+
$image_url = $part->get_file_uri(); // Actual URL. May have limited TTL (often 1 hour).
287+
// TODO: Save as file or so.
288+
break;
228289
}
229290
}
230291

0 commit comments

Comments
 (0)