Skip to content

Commit 2ae60e5

Browse files
authored
feat: add interactive mode to register-gemini-enterprise command (#502)
* feat: add interactive mode to register-gemini-enterprise command Make register-gemini-enterprise user-friendly with interactive prompts when required parameters are missing. ## Changes - Add interactive prompts for Agent Engine ID and Gemini Enterprise app details - Auto-detect Agent Engine ID from deployment_metadata.json with confirmation - Break down GE app ID into simple components (project, location, GE ID) - Construct full resource name automatically from user inputs - Add input validation with helpful error messages - Strip whitespace from all user inputs - Use Rich Console for consistent, clean formatting - Update Makefile help text to reflect interactive usage ## Backward Compatibility All existing environment variables continue to work for CI/CD automation. * docs: update register-gemini-enterprise documentation for interactive mode * refactor: address code review feedback - Replace recursion with while loop in prompt_for_gemini_enterprise_components - Remove unused default_location parameter - Direct error messages to stderr using file=sys.stderr * Fix stderr handling and update test fixtures for Makefile template changes Address code review feedback by properly routing error messages to stderr using a dedicated Rich Console instance. Update test fixtures to reflect the updated Makefile template comments for register-gemini-enterprise. Changes: - Create separate console_err instance with stderr=True for error messages - Regenerate makefile_hashes.json with updated hashes - Update snapshots for adk_base_agent_engine_no_data and adk_a2a_agent_engine
1 parent b783d3f commit 2ae60e5

File tree

7 files changed

+301
-85
lines changed

7 files changed

+301
-85
lines changed

agent_starter_pack/base_template/Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,9 @@ lint:
312312
# ==============================================================================
313313

314314
# Register the deployed agent to Gemini Enterprise
315-
# Usage: ID="projects/.../engines/xxx" make register-gemini-enterprise
315+
# Usage: make register-gemini-enterprise (interactive - will prompt for required IDs)
316+
# The command auto-detects Agent Engine ID from deployment_metadata.json when available
317+
# For non-interactive use, set env vars: ID or GEMINI_ENTERPRISE_APP_ID (full GE resource name)
316318
# Optional env vars: GEMINI_DISPLAY_NAME, GEMINI_DESCRIPTION, GEMINI_TOOL_DESCRIPTION, AGENT_ENGINE_ID
317319
register-gemini-enterprise:
318320
uvx agent-starter-pack@{{ cookiecutter.package_version }} register-gemini-enterprise

agent_starter_pack/cli/commands/register_gemini_enterprise.py

Lines changed: 198 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
import vertexai
2626
from google.auth import default
2727
from google.auth.transport.requests import Request as GoogleAuthRequest
28+
from rich.console import Console
29+
30+
console = Console(highlight=False)
31+
console_err = Console(stderr=True, highlight=False)
2832

2933

3034
def get_discovery_engine_endpoint(location: str) -> str:
@@ -51,35 +55,52 @@ def get_discovery_engine_endpoint(location: str) -> str:
5155
return f"https://{location}-discoveryengine.googleapis.com"
5256

5357

54-
def get_agent_engine_id(
55-
agent_engine_id: str | None, metadata_file: str = "deployment_metadata.json"
56-
) -> str:
57-
"""Get the agent engine ID from parameter or deployment metadata.
58+
def get_agent_engine_id_from_metadata(
59+
metadata_file: str = "deployment_metadata.json",
60+
) -> str | None:
61+
"""Try to read the agent engine ID from deployment metadata.
5862
5963
Args:
60-
agent_engine_id: Optional agent engine resource name
6164
metadata_file: Path to deployment metadata JSON file
6265
6366
Returns:
64-
The agent engine resource name
65-
66-
Raises:
67-
ValueError: If agent_engine_id is not provided and metadata file doesn't exist
67+
The agent engine resource name if found, None otherwise
6868
"""
69-
if agent_engine_id:
70-
return agent_engine_id
71-
72-
# Try to read from deployment_metadata.json
7369
metadata_path = Path(metadata_file)
7470
if not metadata_path.exists():
75-
raise ValueError(
76-
f"No agent engine ID provided and {metadata_file} not found. "
77-
"Please provide --agent-engine-id or deploy your agent first."
78-
)
71+
return None
72+
73+
try:
74+
with open(metadata_path, encoding="utf-8") as f:
75+
metadata = json.load(f)
76+
return metadata.get("remote_agent_engine_id")
77+
except (json.JSONDecodeError, KeyError):
78+
return None
79+
80+
81+
def parse_agent_engine_id(agent_engine_id: str) -> dict[str, str] | None:
82+
"""Parse an Agent Engine resource name to extract components.
83+
84+
Args:
85+
agent_engine_id: Agent Engine resource name
86+
(e.g., projects/PROJECT_NUM/locations/REGION/reasoningEngines/ENGINE_ID)
7987
80-
with open(metadata_path, encoding="utf-8") as f:
81-
metadata = json.load(f)
82-
return metadata["remote_agent_engine_id"]
88+
Returns:
89+
Dictionary with 'project', 'location', 'engine_id' keys, or None if invalid format
90+
"""
91+
parts = agent_engine_id.split("/")
92+
if (
93+
len(parts) == 6
94+
and parts[0] == "projects"
95+
and parts[2] == "locations"
96+
and parts[4] == "reasoningEngines"
97+
):
98+
return {
99+
"project": parts[1],
100+
"location": parts[3],
101+
"engine_id": parts[5],
102+
}
103+
return None
83104

84105

85106
def get_access_token() -> str:
@@ -136,6 +157,101 @@ def get_agent_engine_metadata(agent_engine_id: str) -> tuple[str | None, str | N
136157
return None, None
137158

138159

160+
def prompt_for_agent_engine_id(default_from_metadata: str | None) -> str:
161+
"""Prompt user for Agent Engine ID with optional default.
162+
163+
Args:
164+
default_from_metadata: Default value from deployment_metadata.json if available
165+
166+
Returns:
167+
The Agent Engine resource name
168+
"""
169+
if default_from_metadata:
170+
console.print("\nFound Agent Engine ID from deployment_metadata.json:")
171+
console.print(f" [bold]{default_from_metadata}[/]")
172+
use_default = click.confirm(
173+
"Use this Agent Engine ID?", default=True, show_default=True
174+
)
175+
if use_default:
176+
return default_from_metadata
177+
178+
console.print(
179+
"\nEnter your Agent Engine resource name"
180+
"\n[blue]Example: projects/123456789/locations/us-central1/reasoningEngines/1234567890[/]"
181+
"\n(You can find this in the Agent Builder Console or deployment_metadata.json)"
182+
)
183+
184+
while True:
185+
agent_engine_id = click.prompt("Agent Engine ID", type=str).strip()
186+
parsed = parse_agent_engine_id(agent_engine_id)
187+
if parsed:
188+
return agent_engine_id
189+
else:
190+
console_err.print(
191+
"❌ Invalid format. Expected: projects/{project}/locations/{location}/reasoningEngines/{id}",
192+
style="bold red",
193+
)
194+
195+
196+
def prompt_for_gemini_enterprise_components(
197+
default_project: str | None = None,
198+
) -> str:
199+
"""Prompt user for Gemini Enterprise resource components and construct full ID.
200+
201+
Args:
202+
default_project: Default project number from Agent Engine ID
203+
204+
Returns:
205+
Full Gemini Enterprise app resource name
206+
"""
207+
console.print("\n[blue]" + "=" * 70 + "[/]")
208+
console.print("[blue]GEMINI ENTERPRISE CONFIGURATION[/]")
209+
console.print("[blue]" + "=" * 70 + "[/]")
210+
211+
console.print(
212+
"\nYou need to provide the Gemini Enterprise app details."
213+
"\nFind these in: Google Cloud Console → Gemini Enterprise → Apps"
214+
"\nCopy the ID from the 'ID' column for your Gemini Enterprise instance."
215+
)
216+
217+
while True:
218+
# Project number
219+
if default_project:
220+
console.print(f"\n[dim]Default from Agent Engine: {default_project}[/]")
221+
project_number = click.prompt(
222+
"Project number", type=str, default=default_project or ""
223+
).strip()
224+
225+
# Location - GE apps are typically in 'global', 'us', or 'eu'
226+
console.print("\nGemini Enterprise apps are in: global, us, or eu")
227+
location = click.prompt(
228+
"Location/Region",
229+
type=str,
230+
default="global",
231+
show_default=True,
232+
).strip()
233+
234+
# Gemini Enterprise short ID
235+
console.print(
236+
"\nEnter your Gemini Enterprise ID (from the 'ID' column in the Apps table)."
237+
"\n[blue]Example: gemini-enterprise-1762990_8862980842627[/]"
238+
)
239+
ge_short_id = click.prompt("Gemini Enterprise ID", type=str).strip()
240+
241+
# Construct full resource name
242+
# Format: projects/{project_number}/locations/{location}/collections/default_collection/engines/{ge_id}
243+
full_id = f"projects/{project_number}/locations/{location}/collections/default_collection/engines/{ge_short_id}"
244+
245+
console.print("\nConstructed Gemini Enterprise App ID:")
246+
console.print(f" [bold]{full_id}[/]")
247+
confirmed = click.confirm("Is this correct?", default=True)
248+
249+
if confirmed:
250+
return full_id
251+
252+
click.echo("Let's try again...")
253+
254+
139255
def register_agent(
140256
agent_engine_id: str,
141257
gemini_enterprise_app_id: str,
@@ -237,20 +353,19 @@ def register_agent(
237353
"adk_agent_definition": adk_agent_definition,
238354
}
239355

240-
print("Registering agent to Gemini Enterprise...")
241-
print(f" Agent Engine: {agent_engine_id}")
242-
print(f" Gemini Enterprise App: {gemini_enterprise_app_id}")
243-
print(f" Display Name: {display_name}")
244-
print(f" API Endpoint: {url}")
356+
console.print("\n[blue]Registering agent to Gemini Enterprise...[/]")
357+
console.print(f" Agent Engine: {agent_engine_id}")
358+
console.print(f" Gemini Enterprise App: {gemini_enterprise_app_id}")
359+
console.print(f" Display Name: {display_name}")
245360

246361
try:
247362
# Try to create a new registration first
248363
response = requests.post(url, headers=headers, json=payload, timeout=30)
249364
response.raise_for_status()
250365

251366
result = response.json()
252-
print("\n✅ Successfully registered agent to Gemini Enterprise!")
253-
print(f" Agent Name: {result.get('name', 'N/A')}")
367+
console.print("\n✅ Successfully registered agent to Gemini Enterprise!")
368+
console.print(f" Agent Name: {result.get('name', 'N/A')}")
254369
return result
255370

256371
except requests.exceptions.HTTPError as http_err:
@@ -265,8 +380,8 @@ def register_agent(
265380
"already exists" in error_message.lower()
266381
or "duplicate" in error_message.lower()
267382
):
268-
print(
269-
"\n⚠️ Agent already registered. Updating existing registration..."
383+
console.print(
384+
"\n⚠️ [yellow]Agent already registered. Updating existing registration...[/]"
270385
)
271386

272387
# For update, we need to use the specific agent resource name
@@ -294,7 +409,7 @@ def register_agent(
294409
agent_name = existing_agent.get("name")
295410
update_url = f"{base_endpoint}/v1alpha/{agent_name}"
296411

297-
print(f" Updating agent: {agent_name}")
412+
console.print(f" Updating agent: {agent_name}")
298413

299414
# PATCH request to update
300415
update_response = requests.patch(
@@ -303,27 +418,33 @@ def register_agent(
303418
update_response.raise_for_status()
304419

305420
result = update_response.json()
306-
print(
421+
console.print(
307422
"\n✅ Successfully updated agent registration in Gemini Enterprise!"
308423
)
309-
print(f" Agent Name: {result.get('name', 'N/A')}")
424+
console.print(f" Agent Name: {result.get('name', 'N/A')}")
310425
return result
311426
else:
312-
print(
313-
"\nCould not find existing agent to update",
314-
file=sys.stderr,
427+
console_err.print(
428+
"❌ [red]Could not find existing agent to update[/]",
429+
style="bold red",
315430
)
316431
raise
317432
except (ValueError, KeyError):
318433
# Failed to parse error response, raise original error
319434
pass
320435

321436
# If not an "already exists" error, or update failed, raise the original error
322-
print(f"\n❌ HTTP error occurred: {http_err}", file=sys.stderr)
323-
print(f" Response: {response.text}", file=sys.stderr)
437+
console_err.print(
438+
f"\n❌ [red]HTTP error occurred: {http_err}[/]",
439+
style="bold red",
440+
)
441+
console_err.print(f" Response: {response.text}")
324442
raise
325443
except requests.exceptions.RequestException as req_err:
326-
print(f"\n❌ Request error occurred: {req_err}", file=sys.stderr)
444+
console_err.print(
445+
f"\n❌ [red]Request error occurred: {req_err}[/]",
446+
style="bold red",
447+
)
327448
raise
328449

329450

@@ -343,6 +464,7 @@ def register_agent(
343464
"--gemini-enterprise-app-id",
344465
help="Gemini Enterprise app full resource name "
345466
"(e.g., projects/{project_number}/locations/{location}/collections/{collection}/engines/{engine_id}). "
467+
"If not provided, the command will prompt you interactively. "
346468
"Can also be set via ID or GEMINI_ENTERPRISE_APP_ID env var.",
347469
)
348470
@click.option(
@@ -381,33 +503,58 @@ def register_gemini_enterprise(
381503
project_id: str | None,
382504
authorization_id: str | None,
383505
) -> None:
384-
"""Register an Agent Engine to Gemini Enterprise."""
385-
# Get agent engine ID
386-
try:
387-
resolved_agent_engine_id = get_agent_engine_id(agent_engine_id, metadata_file)
388-
except ValueError as e:
389-
raise click.ClickException(str(e)) from e
506+
"""Register an Agent Engine to Gemini Enterprise.
390507
391-
# Auto-detect display_name and description from Agent Engine
392-
auto_display_name, auto_description = get_agent_engine_metadata(
393-
resolved_agent_engine_id
394-
)
508+
This command can run interactively or accept all parameters via command-line options.
509+
If key parameters are missing, it will prompt the user for input.
510+
"""
511+
console.print("\n🤖 Agent Engine → Gemini Enterprise Registration\n")
512+
513+
# Step 1: Get Agent Engine ID (with smart defaults from deployment_metadata.json)
514+
resolved_agent_engine_id = agent_engine_id
515+
516+
if not resolved_agent_engine_id:
517+
# Check if we have ID from env var (backward compatibility)
518+
env_id = os.getenv("AGENT_ENGINE_ID")
519+
if env_id:
520+
resolved_agent_engine_id = env_id
521+
else:
522+
# Try to get from metadata file
523+
metadata_id = get_agent_engine_id_from_metadata(metadata_file)
524+
# Prompt user (with default if available)
525+
resolved_agent_engine_id = prompt_for_agent_engine_id(metadata_id)
526+
527+
# Validate and parse Agent Engine ID
528+
parsed_ae = parse_agent_engine_id(resolved_agent_engine_id)
529+
if not parsed_ae:
530+
raise click.ClickException(
531+
f"Invalid Agent Engine ID format: {resolved_agent_engine_id}\n"
532+
"Expected: projects/{{project}}/locations/{{location}}/reasoningEngines/{{id}}"
533+
)
395534

396-
# Handle gemini_enterprise_app_id with fallback to ID env var
535+
# Step 2: Get Gemini Enterprise App ID
397536
resolved_gemini_enterprise_app_id = (
398537
gemini_enterprise_app_id
399538
or os.getenv("ID")
400539
or os.getenv("GEMINI_ENTERPRISE_APP_ID")
401540
)
541+
402542
if not resolved_gemini_enterprise_app_id:
403-
raise click.ClickException(
404-
"Error: --gemini-enterprise-app-id or ID/GEMINI_ENTERPRISE_APP_ID env var required"
543+
# Interactive mode: prompt for components and construct the full ID
544+
resolved_gemini_enterprise_app_id = prompt_for_gemini_enterprise_components(
545+
default_project=parsed_ae["project"]
405546
)
406547

548+
# Step 3: Get display name and description (from Agent Engine metadata or defaults)
549+
auto_display_name, auto_description = get_agent_engine_metadata(
550+
resolved_agent_engine_id
551+
)
552+
407553
resolved_display_name = display_name or auto_display_name or "My Agent"
408554
resolved_description = description or auto_description or "AI Agent"
409555
resolved_tool_description = tool_description or resolved_description
410556

557+
# Step 4: Register the agent
411558
try:
412559
register_agent(
413560
agent_engine_id=resolved_agent_engine_id,

0 commit comments

Comments
 (0)