agent-starter-pack 0.18.2__py3-none-any.whl → 0.21.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. agent_starter_pack/agents/{langgraph_base_react → adk_a2a_base}/.template/templateconfig.yaml +5 -12
  2. agent_starter_pack/agents/adk_a2a_base/README.md +37 -0
  3. agent_starter_pack/{frontends/streamlit/frontend/style/app_markdown.py → agents/adk_a2a_base/app/__init__.py} +3 -23
  4. agent_starter_pack/agents/adk_a2a_base/app/agent.py +70 -0
  5. agent_starter_pack/agents/adk_a2a_base/notebooks/adk_a2a_app_testing.ipynb +583 -0
  6. agent_starter_pack/agents/{crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb → adk_a2a_base/notebooks/evaluating_adk_agent.ipynb} +163 -199
  7. agent_starter_pack/agents/adk_a2a_base/tests/integration/test_agent.py +58 -0
  8. agent_starter_pack/agents/adk_base/app/__init__.py +2 -2
  9. agent_starter_pack/agents/adk_base/app/agent.py +3 -0
  10. agent_starter_pack/agents/adk_base/notebooks/adk_app_testing.ipynb +13 -28
  11. agent_starter_pack/agents/adk_live/app/__init__.py +17 -0
  12. agent_starter_pack/agents/adk_live/app/agent.py +3 -0
  13. agent_starter_pack/agents/agentic_rag/app/__init__.py +2 -2
  14. agent_starter_pack/agents/agentic_rag/app/agent.py +3 -0
  15. agent_starter_pack/agents/agentic_rag/notebooks/adk_app_testing.ipynb +13 -28
  16. agent_starter_pack/agents/{crewai_coding_crew → langgraph_base}/.template/templateconfig.yaml +12 -9
  17. agent_starter_pack/agents/langgraph_base/README.md +30 -0
  18. agent_starter_pack/agents/langgraph_base/app/__init__.py +17 -0
  19. agent_starter_pack/agents/{langgraph_base_react → langgraph_base}/app/agent.py +4 -4
  20. agent_starter_pack/agents/{langgraph_base_react → langgraph_base}/tests/integration/test_agent.py +1 -1
  21. agent_starter_pack/base_template/.gitignore +4 -2
  22. agent_starter_pack/base_template/Makefile +110 -16
  23. agent_starter_pack/base_template/README.md +97 -12
  24. agent_starter_pack/base_template/deployment/terraform/dev/apis.tf +4 -6
  25. agent_starter_pack/base_template/deployment/terraform/dev/providers.tf +5 -1
  26. agent_starter_pack/base_template/deployment/terraform/dev/variables.tf +5 -3
  27. agent_starter_pack/base_template/deployment/terraform/dev/{% if cookiecutter.is_adk %}telemetry.tf{% else %}unused_telemetry.tf{% endif %} +193 -0
  28. agent_starter_pack/base_template/deployment/terraform/github.tf +16 -9
  29. agent_starter_pack/base_template/deployment/terraform/locals.tf +7 -7
  30. agent_starter_pack/base_template/deployment/terraform/providers.tf +5 -1
  31. agent_starter_pack/base_template/deployment/terraform/sql/completions.sql +138 -0
  32. agent_starter_pack/base_template/deployment/terraform/storage.tf +0 -9
  33. agent_starter_pack/base_template/deployment/terraform/variables.tf +15 -19
  34. agent_starter_pack/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} +20 -22
  35. agent_starter_pack/base_template/deployment/terraform/{% if cookiecutter.is_adk %}telemetry.tf{% else %}unused_telemetry.tf{% endif %} +206 -0
  36. agent_starter_pack/base_template/pyproject.toml +5 -17
  37. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +19 -4
  38. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +36 -11
  39. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +24 -5
  40. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +44 -9
  41. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/telemetry.py +96 -0
  42. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/{utils → app_utils}/typing.py +4 -6
  43. agent_starter_pack/{agents/crewai_coding_crew/app/crew/config/agents.yaml → base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}converters{% else %}unused_converters{% endif %}/__init__.py } +9 -23
  44. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}converters{% else %}unused_converters{% endif %}/part_converter.py +138 -0
  45. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/__init__.py +13 -0
  46. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/a2a_agent_executor.py +265 -0
  47. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/task_result_aggregator.py +152 -0
  48. agent_starter_pack/cli/commands/create.py +40 -4
  49. agent_starter_pack/cli/commands/enhance.py +1 -1
  50. agent_starter_pack/cli/commands/register_gemini_enterprise.py +1070 -0
  51. agent_starter_pack/cli/main.py +2 -0
  52. agent_starter_pack/cli/utils/cicd.py +20 -4
  53. agent_starter_pack/cli/utils/template.py +257 -25
  54. agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +113 -16
  55. agent_starter_pack/deployment_targets/agent_engine/tests/load_test/README.md +2 -2
  56. agent_starter_pack/deployment_targets/agent_engine/tests/load_test/load_test.py +178 -9
  57. agent_starter_pack/deployment_targets/agent_engine/tests/{% if cookiecutter.is_a2a %}helpers.py{% else %}unused_helpers.py{% endif %} +138 -0
  58. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +193 -307
  59. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/app_utils/deploy.py +414 -0
  60. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/{utils → app_utils}/{% if cookiecutter.is_adk_live %}expose_app.py{% else %}unused_expose_app.py{% endif %} +13 -14
  61. agent_starter_pack/deployment_targets/cloud_run/Dockerfile +4 -1
  62. agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +85 -86
  63. agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/service.tf +139 -107
  64. agent_starter_pack/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +228 -12
  65. agent_starter_pack/deployment_targets/cloud_run/tests/load_test/README.md +4 -4
  66. agent_starter_pack/deployment_targets/cloud_run/tests/load_test/load_test.py +92 -12
  67. agent_starter_pack/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/{server.py → fast_api_app.py} +194 -121
  68. agent_starter_pack/frontends/adk_live_react/frontend/package-lock.json +18 -18
  69. agent_starter_pack/frontends/adk_live_react/frontend/src/multimodal-live-types.ts +5 -3
  70. agent_starter_pack/resources/docs/adk-cheatsheet.md +198 -41
  71. agent_starter_pack/resources/locks/uv-adk_a2a_base-agent_engine.lock +4966 -0
  72. agent_starter_pack/resources/locks/uv-adk_a2a_base-cloud_run.lock +5011 -0
  73. agent_starter_pack/resources/locks/uv-adk_base-agent_engine.lock +1443 -709
  74. agent_starter_pack/resources/locks/uv-adk_base-cloud_run.lock +1058 -874
  75. agent_starter_pack/resources/locks/uv-adk_live-agent_engine.lock +1443 -709
  76. agent_starter_pack/resources/locks/uv-adk_live-cloud_run.lock +1058 -874
  77. agent_starter_pack/resources/locks/uv-agentic_rag-agent_engine.lock +1568 -749
  78. agent_starter_pack/resources/locks/uv-agentic_rag-cloud_run.lock +1123 -929
  79. agent_starter_pack/resources/locks/{uv-langgraph_base_react-agent_engine.lock → uv-langgraph_base-agent_engine.lock} +1714 -1689
  80. agent_starter_pack/resources/locks/{uv-langgraph_base_react-cloud_run.lock → uv-langgraph_base-cloud_run.lock} +1285 -2374
  81. agent_starter_pack/utils/watch_and_rebuild.py +1 -1
  82. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/METADATA +3 -6
  83. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/RECORD +89 -93
  84. agent_starter_pack-0.21.0.dist-info/entry_points.txt +2 -0
  85. llm.txt +4 -5
  86. agent_starter_pack/agents/crewai_coding_crew/README.md +0 -34
  87. agent_starter_pack/agents/crewai_coding_crew/app/agent.py +0 -47
  88. agent_starter_pack/agents/crewai_coding_crew/app/crew/config/tasks.yaml +0 -37
  89. agent_starter_pack/agents/crewai_coding_crew/app/crew/crew.py +0 -71
  90. agent_starter_pack/agents/crewai_coding_crew/tests/integration/test_agent.py +0 -47
  91. agent_starter_pack/agents/langgraph_base_react/README.md +0 -9
  92. agent_starter_pack/agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +0 -1574
  93. agent_starter_pack/base_template/deployment/terraform/dev/log_sinks.tf +0 -69
  94. agent_starter_pack/base_template/deployment/terraform/log_sinks.tf +0 -79
  95. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +0 -155
  96. agent_starter_pack/cli/utils/register_gemini_enterprise.py +0 -406
  97. agent_starter_pack/deployment_targets/agent_engine/deployment/terraform/{% if not cookiecutter.is_adk_live %}service.tf{% else %}unused_service.tf{% endif %} +0 -82
  98. agent_starter_pack/deployment_targets/agent_engine/notebooks/intro_agent_engine.ipynb +0 -1025
  99. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +0 -99
  100. agent_starter_pack/frontends/streamlit/frontend/side_bar.py +0 -214
  101. agent_starter_pack/frontends/streamlit/frontend/streamlit_app.py +0 -265
  102. agent_starter_pack/frontends/streamlit/frontend/utils/chat_utils.py +0 -67
  103. agent_starter_pack/frontends/streamlit/frontend/utils/local_chat_history.py +0 -127
  104. agent_starter_pack/frontends/streamlit/frontend/utils/message_editing.py +0 -59
  105. agent_starter_pack/frontends/streamlit/frontend/utils/multimodal_utils.py +0 -217
  106. agent_starter_pack/frontends/streamlit/frontend/utils/stream_handler.py +0 -310
  107. agent_starter_pack/frontends/streamlit/frontend/utils/title_summary.py +0 -94
  108. agent_starter_pack/resources/locks/uv-crewai_coding_crew-agent_engine.lock +0 -6650
  109. agent_starter_pack/resources/locks/uv-crewai_coding_crew-cloud_run.lock +0 -7825
  110. agent_starter_pack-0.18.2.dist-info/entry_points.txt +0 -3
  111. /agent_starter_pack/agents/{crewai_coding_crew → langgraph_base}/notebooks/evaluating_langgraph_agent.ipynb +0 -0
  112. /agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/{utils → app_utils}/gcs.py +0 -0
  113. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/WHEEL +0 -0
  114. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,7 @@ from rich.console import Console
21
21
  from .commands.create import create
22
22
  from .commands.enhance import enhance
23
23
  from .commands.list import list_agents
24
+ from .commands.register_gemini_enterprise import register_gemini_enterprise
24
25
  from .commands.setup_cicd import setup_cicd
25
26
  from .utils import display_update_message
26
27
 
@@ -57,6 +58,7 @@ def cli() -> None:
57
58
  # Register commands
58
59
  cli.add_command(create)
59
60
  cli.add_command(enhance)
61
+ cli.add_command(register_gemini_enterprise)
60
62
  cli.add_command(setup_cicd)
61
63
  cli.add_command(list_agents, name="list")
62
64
 
@@ -103,9 +103,10 @@ def create_github_connection(
103
103
  """
104
104
  console.print("\n🔗 Creating GitHub connection...")
105
105
 
106
- # First, ensure Cloud Build API is enabled
107
- console.print("🔧 Ensuring Cloud Build API is enabled...")
106
+ # First, ensure required APIs are enabled
107
+ console.print("🔧 Ensuring required APIs are enabled...")
108
108
  try:
109
+ # Enable Cloud Build API
109
110
  run_command(
110
111
  [
111
112
  "gcloud",
@@ -120,13 +121,28 @@ def create_github_connection(
120
121
  )
121
122
  console.print("✅ Cloud Build API enabled")
122
123
 
123
- # Wait for the API to fully initialize and create the service account
124
+ # Enable Secret Manager API
125
+ run_command(
126
+ [
127
+ "gcloud",
128
+ "services",
129
+ "enable",
130
+ "secretmanager.googleapis.com",
131
+ "--project",
132
+ project_id,
133
+ ],
134
+ capture_output=True,
135
+ check=False, # Don't fail if already enabled
136
+ )
137
+ console.print("✅ Secret Manager API enabled")
138
+
139
+ # Wait for the APIs to fully initialize and create the service account
124
140
  console.print(
125
141
  "⏳ Waiting for Cloud Build service account to be created (this typically takes 5-10 seconds)..."
126
142
  )
127
143
  time.sleep(10)
128
144
  except subprocess.CalledProcessError as e:
129
- console.print(f"⚠️ Could not enable Cloud Build API: {e}", style="yellow")
145
+ console.print(f"⚠️ Could not enable required APIs: {e}", style="yellow")
130
146
 
131
147
  # Get the Cloud Build service account and grant permissions with retry logic
132
148
  try:
@@ -16,7 +16,10 @@ import json
16
16
  import logging
17
17
  import os
18
18
  import pathlib
19
+ import re
19
20
  import shutil
21
+ import subprocess
22
+ import sys
20
23
  import tempfile
21
24
  from dataclasses import dataclass
22
25
  from typing import Any
@@ -24,7 +27,7 @@ from typing import Any
24
27
  import yaml
25
28
  from cookiecutter.main import cookiecutter
26
29
  from rich.console import Console
27
- from rich.prompt import IntPrompt, Prompt
30
+ from rich.prompt import Confirm, IntPrompt, Prompt
28
31
 
29
32
  from agent_starter_pack.cli.utils.version import get_current_version
30
33
 
@@ -35,6 +38,105 @@ from .remote_template import (
35
38
  )
36
39
 
37
40
 
41
+ def add_base_template_dependencies_interactively(
42
+ project_path: pathlib.Path,
43
+ base_dependencies: list[str],
44
+ base_template_name: str,
45
+ auto_approve: bool = False,
46
+ ) -> bool:
47
+ """Interactively add base template dependencies using uv add.
48
+
49
+ Args:
50
+ project_path: Path to the project directory
51
+ base_dependencies: List of dependencies from base template's extra_dependencies
52
+ base_template_name: Name of the base template being used
53
+ auto_approve: Whether to skip confirmation and auto-install
54
+
55
+ Returns:
56
+ True if dependencies were added successfully, False otherwise
57
+ """
58
+ if not base_dependencies:
59
+ return True
60
+
61
+ console = Console()
62
+
63
+ # Construct dependency string once for reuse
64
+ deps_str = " ".join(f"'{dep}'" for dep in base_dependencies)
65
+
66
+ # Show what dependencies will be added
67
+ console.print(
68
+ f"\n✓ Base template override: Using '{base_template_name}' as foundation",
69
+ style="bold cyan",
70
+ )
71
+ console.print(" This requires adding the following dependencies:", style="white")
72
+ for dep in base_dependencies:
73
+ console.print(f" • {dep}", style="yellow")
74
+
75
+ # Ask for confirmation unless auto-approve
76
+ should_add = True
77
+ if not auto_approve:
78
+ should_add = Confirm.ask(
79
+ "\n? Add these dependencies automatically?", default=True
80
+ )
81
+
82
+ if not should_add:
83
+ console.print("\n⚠️ Skipped dependency installation.", style="yellow")
84
+ console.print(" To add them manually later, run:", style="dim")
85
+ console.print(f" cd {project_path.name}", style="dim")
86
+ console.print(f" uv add {deps_str}\n", style="dim")
87
+ return False
88
+
89
+ # Run uv add
90
+ try:
91
+ if auto_approve:
92
+ console.print(
93
+ f"✓ Auto-installing dependencies: {', '.join(base_dependencies)}",
94
+ style="bold cyan",
95
+ )
96
+ else:
97
+ console.print(f"\n✓ Running: uv add {deps_str}", style="bold cyan")
98
+
99
+ # Run uv add in the project directory
100
+ cmd = ["uv", "add"] + base_dependencies
101
+ result = subprocess.run(
102
+ cmd,
103
+ cwd=project_path,
104
+ capture_output=True,
105
+ text=True,
106
+ check=True,
107
+ )
108
+
109
+ # Show success message
110
+ if not auto_approve:
111
+ # Show a summary line from uv output
112
+ output_lines = result.stderr.strip().split("\n")
113
+ for line in output_lines:
114
+ if "Resolved" in line or "Installed" in line:
115
+ console.print(f" {line}", style="dim")
116
+ break
117
+
118
+ console.print("✓ Dependencies added successfully\n", style="bold green")
119
+ return True
120
+
121
+ except subprocess.CalledProcessError as e:
122
+ console.print(
123
+ f"\n✗ Failed to add dependencies: {e.stderr.strip()}", style="bold red"
124
+ )
125
+ console.print(" You can add them manually:", style="yellow")
126
+ console.print(f" cd {project_path.name}", style="dim")
127
+ console.print(f" uv add {deps_str}\n", style="dim")
128
+ return False
129
+ except FileNotFoundError:
130
+ console.print(
131
+ "\n✗ uv command not found. Please install uv first.", style="bold red"
132
+ )
133
+ console.print(" Install from: https://docs.astral.sh/uv/", style="dim")
134
+ console.print("\n To add dependencies manually:", style="yellow")
135
+ console.print(f" cd {project_path.name}", style="dim")
136
+ console.print(f" uv add {deps_str}\n", style="dim")
137
+ return False
138
+
139
+
38
140
  def validate_agent_directory_name(agent_dir: str) -> None:
39
141
  """Validate that an agent directory name is a valid Python identifier.
40
142
 
@@ -112,9 +214,10 @@ def get_available_agents(deployment_target: str | None = None) -> dict:
112
214
  # Define priority agents that should appear first
113
215
  PRIORITY_AGENTS = [
114
216
  "adk_base",
217
+ "adk_a2a_base",
115
218
  "adk_live",
116
219
  "agentic_rag",
117
- "langgraph_base_react",
220
+ "langgraph_base",
118
221
  ]
119
222
 
120
223
  agents_list = []
@@ -251,9 +354,9 @@ def prompt_session_type_selection() -> str:
251
354
  "display_name": "In-memory session",
252
355
  "description": "Session data stored in memory - ideal for stateless applications",
253
356
  },
254
- "alloydb": {
255
- "display_name": "AlloyDB",
256
- "description": "Use AlloyDB for session management. Comes with terraform resources for deployment.",
357
+ "cloud_sql": {
358
+ "display_name": "Cloud SQL (PostgreSQL)",
359
+ "description": "Managed PostgreSQL database for robust session persistence",
257
360
  },
258
361
  "agent_engine": {
259
362
  "display_name": "Vertex AI Agent Engine",
@@ -458,6 +561,113 @@ def copy_data_ingestion_files(
458
561
  )
459
562
 
460
563
 
564
+ def _extract_agent_garden_labels(
565
+ agent_garden: bool,
566
+ remote_spec: Any | None,
567
+ remote_template_path: pathlib.Path | None,
568
+ ) -> tuple[str | None, str | None]:
569
+ """Extract agent sample ID and publisher for Agent Garden labeling.
570
+
571
+ This function supports two mechanisms for extracting label information:
572
+ 1. From remote_spec metadata (for ADK samples)
573
+ 2. Fallback to pyproject.toml parsing (for version-locked templates)
574
+
575
+ Args:
576
+ agent_garden: Whether this deployment is from Agent Garden
577
+ remote_spec: Remote template spec with ADK samples metadata
578
+ remote_template_path: Path to remote template directory
579
+
580
+ Returns:
581
+ Tuple of (agent_sample_id, agent_sample_publisher) or (None, None) if no labels found
582
+ """
583
+ if not agent_garden:
584
+ return None, None
585
+
586
+ agent_sample_id = None
587
+ agent_sample_publisher = None
588
+
589
+ # Handle remote specs with ADK samples metadata
590
+ if (
591
+ remote_spec
592
+ and hasattr(remote_spec, "is_adk_samples")
593
+ and remote_spec.is_adk_samples
594
+ ):
595
+ # For ADK samples, template_path is like "python/agents/sample-name"
596
+ agent_sample_id = pathlib.Path(remote_spec.template_path).name
597
+ # For ADK samples, publisher is always "google"
598
+ agent_sample_publisher = "google"
599
+ logging.debug(f"Detected ADK sample from remote_spec: {agent_sample_id}")
600
+ return agent_sample_id, agent_sample_publisher
601
+
602
+ # Fallback: Detect ADK samples from pyproject.toml (for version-locked templates)
603
+ if remote_template_path:
604
+ pyproject_path = remote_template_path / "pyproject.toml"
605
+ if pyproject_path.exists():
606
+ try:
607
+ if sys.version_info >= (3, 11):
608
+ import tomllib
609
+ else:
610
+ import tomli as tomllib
611
+
612
+ with open(pyproject_path, "rb") as toml_file:
613
+ pyproject_data = tomllib.load(toml_file)
614
+
615
+ # Extract project name from pyproject.toml
616
+ project_name_from_toml = pyproject_data.get("project", {}).get("name")
617
+
618
+ if project_name_from_toml:
619
+ agent_sample_id = project_name_from_toml
620
+ agent_sample_publisher = "google" # ADK samples are from Google
621
+ logging.debug(
622
+ f"Detected ADK sample from pyproject.toml: {agent_sample_id}"
623
+ )
624
+ except Exception as e:
625
+ logging.debug(f"Failed to read pyproject.toml: {e}")
626
+
627
+ return agent_sample_id, agent_sample_publisher
628
+
629
+
630
+ def _inject_app_object_if_missing(
631
+ agent_py_path: pathlib.Path, agent_directory: str, console: Console
632
+ ) -> None:
633
+ """Inject app object into agent.py if missing (backward compatibility for ADK).
634
+
635
+ Args:
636
+ agent_py_path: Path to the agent.py file
637
+ agent_directory: Name of the agent directory for logging
638
+ console: Rich console for user feedback
639
+ """
640
+ try:
641
+ content = agent_py_path.read_text(encoding="utf-8")
642
+ # Check for app object (assignment, function definition, or import)
643
+ app_patterns = [
644
+ r"^\s*app\s*=", # assignment: app = ...
645
+ r"^\s*def\s+app\(", # function: def app(...)
646
+ r"from\s+.*\s+import\s+.*\bapp\b", # import: from ... import app
647
+ ]
648
+ has_app = any(
649
+ re.search(pattern, content, re.MULTILINE) for pattern in app_patterns
650
+ )
651
+
652
+ if not has_app:
653
+ console.print(
654
+ f"ℹ️ Adding 'app' object to [cyan]{agent_directory}/agent.py[/cyan] for backward compatibility",
655
+ style="dim",
656
+ )
657
+ # Add import and app object at the end of the file
658
+ content = content.rstrip()
659
+ if "from google.adk.apps.app import App" not in content:
660
+ content += "\n\nfrom google.adk.apps.app import App\n"
661
+ content += '\napp = App(root_agent=root_agent, name="app")\n'
662
+
663
+ # Write the modified content back
664
+ agent_py_path.write_text(content, encoding="utf-8")
665
+ except Exception as e:
666
+ logging.warning(
667
+ f"Could not inject app object into {agent_directory}/agent.py: {type(e).__name__}: {e}"
668
+ )
669
+
670
+
461
671
  def process_template(
462
672
  agent_name: str,
463
673
  template_dir: pathlib.Path,
@@ -498,6 +708,9 @@ def process_template(
498
708
  logging.debug(f"Include pipeline: {datastore}")
499
709
  logging.debug(f"Output directory: {output_dir}")
500
710
 
711
+ # Create console for user feedback
712
+ console = Console()
713
+
501
714
  def get_agent_directory(
502
715
  template_config: dict[str, Any], cli_overrides: dict[str, Any] | None = None
503
716
  ) -> str:
@@ -559,13 +772,9 @@ def process_template(
559
772
  os.chdir(temp_path) # Change to temp directory
560
773
 
561
774
  # Extract agent sample info for labeling when using agent garden with remote templates
562
- agent_sample_id = None
563
- agent_sample_publisher = None
564
- if agent_garden and remote_spec and remote_spec.is_adk_samples:
565
- # For ADK samples, template_path is like "python/agents/sample-name"
566
- agent_sample_id = pathlib.Path(remote_spec.template_path).name
567
- # For ADK samples, publisher is always "google"
568
- agent_sample_publisher = "google"
775
+ agent_sample_id, agent_sample_publisher = _extract_agent_garden_labels(
776
+ agent_garden, remote_spec, remote_template_path
777
+ )
569
778
 
570
779
  # Create the cookiecutter template structure
571
780
  cookiecutter_template = temp_path / "template"
@@ -717,14 +926,14 @@ def process_template(
717
926
  / "docs"
718
927
  / "adk-cheatsheet.md"
719
928
  )
720
- with open(adk_cheatsheet_path, encoding="utf-8") as f:
721
- adk_cheatsheet_content = f.read()
929
+ with open(adk_cheatsheet_path, encoding="utf-8") as md_file:
930
+ adk_cheatsheet_content = md_file.read()
722
931
 
723
932
  llm_txt_path = (
724
933
  pathlib.Path(__file__).parent.parent.parent.parent / "llm.txt"
725
934
  )
726
- with open(llm_txt_path, encoding="utf-8") as f:
727
- llm_txt_content = f.read()
935
+ with open(llm_txt_path, encoding="utf-8") as txt_file:
936
+ llm_txt_content = txt_file.read()
728
937
 
729
938
  cookiecutter_config = {
730
939
  "project_name": project_name,
@@ -738,6 +947,7 @@ def process_template(
738
947
  "tags": tags,
739
948
  "is_adk": "adk" in tags,
740
949
  "is_adk_live": "adk_live" in tags,
950
+ "is_a2a": "a2a" in tags,
741
951
  "deployment_target": deployment_target or "",
742
952
  "cicd_runner": cicd_runner or "google_cloud_build",
743
953
  "session_type": session_type or "",
@@ -777,8 +987,8 @@ def process_template(
777
987
 
778
988
  with open(
779
989
  cookiecutter_template / "cookiecutter.json", "w", encoding="utf-8"
780
- ) as f:
781
- json.dump(cookiecutter_config, f, indent=4)
990
+ ) as json_file:
991
+ json.dump(cookiecutter_config, json_file, indent=4)
782
992
 
783
993
  logging.debug(f"Template structure created at {cookiecutter_template}")
784
994
  logging.debug(
@@ -859,6 +1069,19 @@ def process_template(
859
1069
  )
860
1070
  logging.debug("Remote template files copied successfully")
861
1071
 
1072
+ # Inject app object if missing (backward compatibility for ADK remote templates)
1073
+ # Only inject for ADK agents with agent_engine deployment
1074
+ is_adk = "adk" in tags
1075
+ agent_py_path = generated_project_dir / agent_directory / "agent.py"
1076
+ if (
1077
+ is_adk
1078
+ and agent_py_path.exists()
1079
+ and deployment_target == "agent_engine"
1080
+ ):
1081
+ _inject_app_object_if_missing(
1082
+ agent_py_path, agent_directory, console
1083
+ )
1084
+
862
1085
  # Move the generated project to the final destination
863
1086
  generated_project_dir = temp_path / project_name
864
1087
 
@@ -927,8 +1150,12 @@ def process_template(
927
1150
  file_template_dir / "cookiecutter.json",
928
1151
  "w",
929
1152
  encoding="utf-8",
930
- ) as f:
931
- json.dump(cookiecutter_config, f, indent=4)
1153
+ ) as config_file:
1154
+ json.dump(
1155
+ cookiecutter_config,
1156
+ config_file,
1157
+ indent=4,
1158
+ )
932
1159
 
933
1160
  # Process the file template
934
1161
  cookiecutter(
@@ -1089,13 +1316,13 @@ def process_template(
1089
1316
 
1090
1317
  # Replace cookiecutter project name with actual project name in lock file
1091
1318
  lock_file_path = final_destination / "uv.lock"
1092
- with open(lock_file_path, "r+", encoding="utf-8") as f:
1093
- content = f.read()
1094
- f.seek(0)
1095
- f.write(
1319
+ with open(lock_file_path, "r+", encoding="utf-8") as lock_file:
1320
+ content = lock_file.read()
1321
+ lock_file.seek(0)
1322
+ lock_file.write(
1096
1323
  content.replace("{{cookiecutter.project_name}}", project_name)
1097
1324
  )
1098
- f.truncate()
1325
+ lock_file.truncate()
1099
1326
  logging.debug(f"Updated project name in lock file at {lock_file_path}")
1100
1327
 
1101
1328
  except Exception as e:
@@ -1185,6 +1412,11 @@ def copy_frontend_files(frontend_type: str, project_template: pathlib.Path) -> N
1185
1412
  logging.debug("Frontend type is 'None' or empty, skipping frontend files")
1186
1413
  return
1187
1414
 
1415
+ # Skip copying if frontend_type is "inspector" - it's installed at runtime via make inspector
1416
+ if frontend_type == "inspector":
1417
+ logging.debug("Frontend type is 'inspector', skipping (installed at runtime)")
1418
+ return
1419
+
1188
1420
  # Get the frontends directory path
1189
1421
  frontends_path = (
1190
1422
  pathlib.Path(__file__).parent.parent.parent / "frontends" / frontend_type
@@ -47,7 +47,7 @@ def start_server() -> subprocess.Popen[str]:
47
47
  sys.executable,
48
48
  "-m",
49
49
  "uvicorn",
50
- "app.utils.expose_app:app",
50
+ "app.app_utils.expose_app:app",
51
51
  "--host",
52
52
  "0.0.0.0",
53
53
  "--port",
@@ -202,8 +202,8 @@ def test_feedback_endpoint(server_fixture: subprocess.Popen[str]) -> None:
202
202
  feedback_data = {
203
203
  "score": 5,
204
204
  "text": "Great response!",
205
- "run_id": "test-run-123",
206
- "user_id": "test-user",
205
+ "user_id": "test-user-123",
206
+ "session_id": "test-session-123",
207
207
  "log_type": "feedback",
208
208
  }
209
209
 
@@ -213,32 +213,124 @@ def test_feedback_endpoint(server_fixture: subprocess.Popen[str]) -> None:
213
213
  logger.info("Feedback endpoint test passed")
214
214
  {% else %}
215
215
 
216
+ # mypy: disable-error-code="arg-type"
217
+ {%- if cookiecutter.is_a2a %}
218
+
219
+ import os
220
+
221
+ import pytest
222
+
223
+ from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
224
+ from tests.helpers import (
225
+ build_get_request,
226
+ build_post_request,
227
+ poll_task_completion,
228
+ )
229
+ {%- elif cookiecutter.is_adk %}
230
+
216
231
  import logging
217
232
 
218
233
  import pytest
219
- {%- if cookiecutter.is_adk %}
220
234
  from google.adk.events.event import Event
221
235
 
222
- from {{cookiecutter.agent_directory}}.agent import root_agent
223
236
  from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
224
237
  {%- else %}
225
238
 
239
+ import logging
240
+
241
+ import pytest
242
+
226
243
  from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
227
244
  {%- endif %}
245
+ {%- if cookiecutter.is_a2a %}
228
246
 
229
247
 
230
248
  @pytest.fixture
231
249
  def agent_app() -> AgentEngineApp:
232
250
  """Fixture to create and set up AgentEngineApp instance"""
233
- {%- if cookiecutter.is_adk %}
234
- app = AgentEngineApp(agent=root_agent)
251
+ from {{cookiecutter.agent_directory}}.agent_engine_app import agent_engine
252
+
253
+ agent_engine.set_up()
254
+ return agent_engine
235
255
  {%- else %}
236
- app = AgentEngineApp()
237
- {%- endif %}
238
- app.set_up()
239
- return app
240
256
 
241
- {% if cookiecutter.is_adk %}
257
+
258
+ @pytest.fixture
259
+ def agent_app() -> AgentEngineApp:
260
+ """Fixture to create and set up AgentEngineApp instance"""
261
+ from {{cookiecutter.agent_directory}}.agent_engine_app import agent_engine
262
+
263
+ agent_engine.set_up()
264
+ return agent_engine
265
+ {% endif %}
266
+ {%- if cookiecutter.is_a2a %}
267
+
268
+
269
+ @pytest.mark.asyncio
270
+ async def test_agent_on_message_send(agent_app: AgentEngineApp) -> None:
271
+ """Test complete A2A message workflow from send to task completion with artifacts."""
272
+ # Send message
273
+ message_data = {
274
+ "message": {
275
+ "messageId": f"msg-{os.urandom(8).hex()}",
276
+ "content": [{"text": "What is the capital of France?"}],
277
+ "role": "ROLE_USER",
278
+ },
279
+ }
280
+ response = await agent_app.on_message_send(
281
+ request=build_post_request(message_data),
282
+ context=None,
283
+ )
284
+
285
+ # Verify task creation
286
+ assert "task" in response and "id" in response["task"], (
287
+ "Expected task with ID in response"
288
+ )
289
+
290
+ # Poll for completion
291
+ final_response = await poll_task_completion(agent_app, response["task"]["id"])
292
+
293
+ # Verify artifacts
294
+ assert final_response.get("artifacts"), "Expected artifacts in completed task"
295
+ artifact = final_response["artifacts"][0]
296
+ assert artifact.get("parts") and artifact["parts"][0].get("text"), (
297
+ "Expected artifact with text content"
298
+ )
299
+
300
+
301
+ @pytest.mark.asyncio
302
+ async def test_agent_card(agent_app: AgentEngineApp) -> None:
303
+ """Test agent card retrieval and validation of required A2A fields."""
304
+ response = await agent_app.handle_authenticated_agent_card(
305
+ request=build_get_request(None),
306
+ context=None,
307
+ )
308
+
309
+ # Verify core agent card fields
310
+ assert response.get("name") == "root_agent", "Expected agent name 'root_agent'"
311
+ assert response.get("protocolVersion") == "0.3.0", "Expected protocol version 0.3.0"
312
+ assert response.get("preferredTransport") == "HTTP+JSON", (
313
+ "Expected HTTP+JSON transport"
314
+ )
315
+
316
+ # Verify capabilities
317
+ capabilities = response.get("capabilities", {})
318
+ assert capabilities.get("streaming") is False, "Expected streaming disabled"
319
+
320
+ # Verify skills
321
+ skills = response.get("skills", [])
322
+ assert len(skills) > 0, "Expected at least one skill"
323
+ for skill in skills:
324
+ assert all(key in skill for key in ["id", "name", "description"]), (
325
+ "Expected id, name, and description in each skill"
326
+ )
327
+
328
+ # Verify extended card support
329
+ assert response.get("supportsAuthenticatedExtendedCard") is True, (
330
+ "Expected supportsAuthenticatedExtendedCard to be True"
331
+ )
332
+ {% elif cookiecutter.is_adk %}
333
+
242
334
  @pytest.mark.asyncio
243
335
  async def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
244
336
  """
@@ -276,7 +368,8 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
276
368
  feedback_data = {
277
369
  "score": 5,
278
370
  "text": "Great response!",
279
- "invocation_id": "test-run-123",
371
+ "user_id": "test-user-456",
372
+ "session_id": "test-session-456",
280
373
  }
281
374
 
282
375
  # Should not raise any exceptions
@@ -287,12 +380,14 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
287
380
  invalid_feedback = {
288
381
  "score": "invalid", # Score must be numeric
289
382
  "text": "Bad feedback",
290
- "invocation_id": "test-run-123",
383
+ "user_id": "test-user-789",
384
+ "session_id": "test-session-789",
291
385
  }
292
386
  agent_app.register_feedback(invalid_feedback)
293
387
 
294
388
  logging.info("All assertions passed for agent feedback test")
295
389
  {% else %}
390
+
296
391
  def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
297
392
  """
298
393
  Integration test for the agent stream query functionality.
@@ -368,7 +463,8 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
368
463
  feedback_data = {
369
464
  "score": 5,
370
465
  "text": "Great response!",
371
- "run_id": "test-run-123",
466
+ "user_id": "test-user-456",
467
+ "session_id": "test-session-456",
372
468
  }
373
469
 
374
470
  # Should not raise any exceptions
@@ -379,7 +475,8 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
379
475
  invalid_feedback = {
380
476
  "score": "invalid", # Score must be numeric
381
477
  "text": "Bad feedback",
382
- "run_id": "test-run-123",
478
+ "user_id": "test-user-789",
479
+ "session_id": "test-session-789",
383
480
  }
384
481
  agent_app.register_feedback(invalid_feedback)
385
482
 
@@ -16,13 +16,13 @@ The load test simulates realistic user interactions by:
16
16
  Launch the expose app server in a separate terminal, pointing to your deployed agent engine:
17
17
 
18
18
  ```bash
19
- uv run python -m app.utils.expose_app --mode remote --remote-id <your-agent-engine-id>
19
+ uv run python -m app.app_utils.expose_app --mode remote --remote-id <your-agent-engine-id>
20
20
  ```
21
21
 
22
22
  Or if you have `deployment_metadata.json` in your project root:
23
23
 
24
24
  ```bash
25
- uv run python -m app.utils.expose_app --mode remote
25
+ uv run python -m app.app_utils.expose_app --mode remote
26
26
  ```
27
27
 
28
28
  **2. Execute the Load Test:**