agent-starter-pack 0.9.1__py3-none-any.whl → 0.10.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 (57) hide show
  1. {agent_starter_pack-0.9.1.dist-info → agent_starter_pack-0.10.0.dist-info}/METADATA +5 -7
  2. {agent_starter_pack-0.9.1.dist-info → agent_starter_pack-0.10.0.dist-info}/RECORD +54 -52
  3. agents/adk_base/.template/templateconfig.yaml +1 -1
  4. agents/adk_gemini_fullstack/.template/templateconfig.yaml +1 -1
  5. agents/agentic_rag/.template/templateconfig.yaml +1 -1
  6. agents/agentic_rag/README.md +1 -1
  7. agents/live_api/tests/integration/test_server_e2e.py +7 -1
  8. llm.txt +3 -2
  9. src/base_template/Makefile +4 -3
  10. src/base_template/README.md +7 -2
  11. src/base_template/deployment/README.md +6 -121
  12. src/base_template/deployment/terraform/github.tf +284 -0
  13. src/base_template/deployment/terraform/providers.tf +5 -0
  14. src/base_template/deployment/terraform/variables.tf +40 -1
  15. src/base_template/deployment/terraform/vars/env.tfvars +7 -1
  16. src/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'github_actions' %}wif.tf{% else %}unused_wif.tf{% endif %} +43 -0
  17. src/base_template/deployment/terraform/{build_triggers.tf → {% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} } +33 -18
  18. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +114 -0
  19. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +65 -0
  20. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +170 -0
  21. src/base_template/{deployment/cd/deploy-to-prod.yaml → {% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml } +7 -7
  22. src/base_template/{deployment/cd/staging.yaml → {% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml } +7 -7
  23. src/cli/commands/create.py +223 -41
  24. src/cli/commands/list.py +4 -10
  25. src/cli/commands/setup_cicd.py +292 -298
  26. src/cli/utils/cicd.py +19 -7
  27. src/cli/utils/remote_template.py +82 -15
  28. src/cli/utils/template.py +79 -19
  29. src/deployment_targets/cloud_run/app/server.py +19 -0
  30. src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +4 -3
  31. src/deployment_targets/cloud_run/deployment/terraform/service.tf +4 -3
  32. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +35 -0
  33. src/deployment_targets/cloud_run/tests/load_test/README.md +1 -1
  34. src/frontends/live_api_react/frontend/package-lock.json +19 -16
  35. src/frontends/streamlit/frontend/side_bar.py +1 -1
  36. src/frontends/streamlit/frontend/utils/chat_utils.py +1 -1
  37. src/frontends/streamlit/frontend/utils/local_chat_history.py +2 -2
  38. src/resources/containers/e2e-tests/Dockerfile +39 -17
  39. src/resources/docs/adk-cheatsheet.md +1 -1
  40. src/resources/locks/uv-adk_base-agent_engine.lock +164 -131
  41. src/resources/locks/uv-adk_base-cloud_run.lock +177 -144
  42. src/resources/locks/uv-adk_gemini_fullstack-agent_engine.lock +164 -131
  43. src/resources/locks/uv-adk_gemini_fullstack-cloud_run.lock +177 -144
  44. src/resources/locks/uv-agentic_rag-agent_engine.lock +223 -190
  45. src/resources/locks/uv-agentic_rag-cloud_run.lock +251 -218
  46. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +315 -485
  47. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +358 -531
  48. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +281 -249
  49. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +323 -290
  50. src/resources/locks/uv-live_api-cloud_run.lock +350 -327
  51. src/resources/setup_cicd/cicd_variables.tf +0 -41
  52. src/resources/setup_cicd/github.tf +0 -87
  53. src/resources/setup_cicd/providers.tf +0 -39
  54. {agent_starter_pack-0.9.1.dist-info → agent_starter_pack-0.10.0.dist-info}/WHEEL +0 -0
  55. {agent_starter_pack-0.9.1.dist-info → agent_starter_pack-0.10.0.dist-info}/entry_points.txt +0 -0
  56. {agent_starter_pack-0.9.1.dist-info → agent_starter_pack-0.10.0.dist-info}/licenses/LICENSE +0 -0
  57. /src/base_template/{deployment/ci → {% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}}/pr_checks.yaml +0 -0
src/cli/utils/cicd.py CHANGED
@@ -125,6 +125,7 @@ def create_github_connection(
125
125
  stdout=subprocess.PIPE,
126
126
  stderr=subprocess.PIPE,
127
127
  text=True,
128
+ encoding="utf-8",
128
129
  )
129
130
 
130
131
  # Send 'y' followed by enter key to handle both the API enablement prompt and any other prompts
@@ -263,12 +264,12 @@ class ProjectConfig:
263
264
  cicd_project_id: str
264
265
  agent: str
265
266
  deployment_target: str
267
+ repository_name: str
268
+ repository_owner: str
266
269
  region: str = "us-central1"
267
270
  dev_project_id: str | None = None
268
271
  project_name: str | None = None
269
- repository_name: str | None = None
270
- repository_owner: str | None = None
271
- repository_exists: bool | None = None
272
+ create_repository: bool | None = None
272
273
  host_connection_name: str | None = None
273
274
  github_pat: str | None = None
274
275
  github_app_installation_id: str | None = None
@@ -405,6 +406,7 @@ def run_command(
405
406
  capture_output: bool = False,
406
407
  shell: bool = False,
407
408
  input: str | None = None,
409
+ env_vars: dict[str, str] | None = None,
408
410
  ) -> subprocess.CompletedProcess:
409
411
  """Run a command and display it to the user"""
410
412
  # Format command for display
@@ -413,6 +415,14 @@ def run_command(
413
415
  if cwd:
414
416
  print(f"📂 In directory: {cwd}")
415
417
 
418
+ # Prepare environment variables
419
+ env = None
420
+ if env_vars:
421
+ import os
422
+
423
+ env = os.environ.copy()
424
+ env.update(env_vars)
425
+
416
426
  # Run the command
417
427
  result = subprocess.run(
418
428
  cmd,
@@ -422,6 +432,7 @@ def run_command(
422
432
  text=True,
423
433
  shell=shell,
424
434
  input=input,
435
+ env=env,
425
436
  )
426
437
 
427
438
  # Display output if captured
@@ -475,6 +486,7 @@ def handle_github_authentication() -> None:
475
486
  stdout=subprocess.PIPE,
476
487
  stderr=subprocess.PIPE,
477
488
  text=True,
489
+ encoding="utf-8",
478
490
  )
479
491
  stdout, stderr = process.communicate(input=token + "\n")
480
492
 
@@ -565,7 +577,7 @@ class E2EDeployment:
565
577
  project_dir / "deployment" / "terraform" / "dev" / "vars" / "env.tfvars"
566
578
  )
567
579
 
568
- with open(tf_vars_path) as f:
580
+ with open(tf_vars_path, encoding="utf-8") as f:
569
581
  content = f.read()
570
582
 
571
583
  # Replace dev project ID
@@ -580,7 +592,7 @@ class E2EDeployment:
580
592
  project_dir / "deployment" / "terraform" / "vars" / "env.tfvars"
581
593
  )
582
594
 
583
- with open(tf_vars_path) as f:
595
+ with open(tf_vars_path, encoding="utf-8") as f:
584
596
  content = f.read()
585
597
 
586
598
  # Replace all project IDs
@@ -613,7 +625,7 @@ class E2EDeployment:
613
625
  )
614
626
 
615
627
  # Write updated content
616
- with open(tf_vars_path, "w") as f:
628
+ with open(tf_vars_path, "w", encoding="utf-8") as f:
617
629
  f.write(content)
618
630
 
619
631
  def setup_terraform_state(self, project_dir: Path, env: Environment) -> None:
@@ -667,7 +679,7 @@ class E2EDeployment:
667
679
  state_prefix = "dev" if is_dev_dir else "prod"
668
680
 
669
681
  backend_file = tf_dir / "backend.tf"
670
- with open(backend_file, "w") as f:
682
+ with open(backend_file, "w", encoding="utf-8") as f:
671
683
  f.write(f'''terraform {{
672
684
  backend "gcs" {{
673
685
  bucket = "{bucket_name}"
@@ -23,6 +23,7 @@ from dataclasses import dataclass
23
23
  from typing import Any
24
24
 
25
25
  import yaml
26
+ from jinja2 import Environment
26
27
 
27
28
 
28
29
  @dataclass
@@ -112,7 +113,9 @@ def parse_agent_spec(agent_spec: str) -> RemoteTemplateSpec | None:
112
113
  return None
113
114
 
114
115
 
115
- def fetch_remote_template(spec: RemoteTemplateSpec) -> pathlib.Path:
116
+ def fetch_remote_template(
117
+ spec: RemoteTemplateSpec,
118
+ ) -> tuple[pathlib.Path, pathlib.Path]:
116
119
  """Fetch remote template and return path to template directory.
117
120
 
118
121
  Uses Git to clone the remote repository.
@@ -121,7 +124,9 @@ def fetch_remote_template(spec: RemoteTemplateSpec) -> pathlib.Path:
121
124
  spec: Remote template specification
122
125
 
123
126
  Returns:
124
- Path to the fetched template directory
127
+ A tuple containing:
128
+ - Path to the fetched template directory.
129
+ - Path to the top-level temporary directory that should be cleaned up.
125
130
  """
126
131
  temp_dir = tempfile.mkdtemp(prefix="asp_remote_template_")
127
132
  temp_path = pathlib.Path(temp_dir)
@@ -169,18 +174,7 @@ def fetch_remote_template(spec: RemoteTemplateSpec) -> pathlib.Path:
169
174
  f"Template path not found in the repository: {spec.template_path}"
170
175
  )
171
176
 
172
- # Exclude Makefile and README.md from remote template to avoid conflicts
173
- makefile_path = template_dir / "Makefile"
174
- if makefile_path.exists():
175
- logging.debug(f"Removing Makefile from remote template: {makefile_path}")
176
- makefile_path.unlink()
177
-
178
- readme_path = template_dir / "README.md"
179
- if readme_path.exists():
180
- logging.debug(f"Removing README.md from remote template: {readme_path}")
181
- readme_path.unlink()
182
-
183
- return template_dir
177
+ return template_dir, temp_path
184
178
  except Exception as e:
185
179
  # Clean up on error
186
180
  shutil.rmtree(temp_path, ignore_errors=True)
@@ -204,7 +198,7 @@ def load_remote_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
204
198
  return {}
205
199
 
206
200
  try:
207
- with open(config_path) as f:
201
+ with open(config_path, encoding="utf-8") as f:
208
202
  config = yaml.safe_load(f)
209
203
  return config if config else {}
210
204
  except Exception as e:
@@ -252,3 +246,76 @@ def merge_template_configs(
252
246
 
253
247
  # Perform the deep merge
254
248
  return deep_merge(merged_config, remote_config)
249
+
250
+
251
+ def render_and_merge_makefiles(
252
+ base_template_path: pathlib.Path,
253
+ final_destination: pathlib.Path,
254
+ cookiecutter_config: dict,
255
+ remote_template_path: pathlib.Path | None = None,
256
+ ) -> None:
257
+ """
258
+ Renders the base and remote Makefiles separately, then merges them.
259
+
260
+ If remote_template_path is not provided, only the base Makefile is rendered.
261
+ """
262
+
263
+ env = Environment()
264
+
265
+ # Render the base Makefile
266
+ base_makefile_path = base_template_path / "Makefile"
267
+ if base_makefile_path.exists():
268
+ with open(base_makefile_path, encoding="utf-8") as f:
269
+ base_template = env.from_string(f.read())
270
+ rendered_base_makefile = base_template.render(cookiecutter=cookiecutter_config)
271
+ else:
272
+ rendered_base_makefile = ""
273
+
274
+ # Render the remote Makefile if a path is provided
275
+ rendered_remote_makefile = ""
276
+ if remote_template_path:
277
+ remote_makefile_path = remote_template_path / "Makefile"
278
+ if remote_makefile_path.exists():
279
+ with open(remote_makefile_path, encoding="utf-8") as f:
280
+ remote_template = env.from_string(f.read())
281
+ rendered_remote_makefile = remote_template.render(
282
+ cookiecutter=cookiecutter_config
283
+ )
284
+
285
+ # Merge the rendered Makefiles
286
+ if rendered_base_makefile and rendered_remote_makefile:
287
+ # A simple merge: remote content first, then append missing commands from base
288
+ base_commands = set(
289
+ re.findall(r"^([a-zA-Z0-9_-]+):", rendered_base_makefile, re.MULTILINE)
290
+ )
291
+ remote_commands = set(
292
+ re.findall(r"^([a-zA-Z0-9_-]+):", rendered_remote_makefile, re.MULTILINE)
293
+ )
294
+ missing_commands = base_commands - remote_commands
295
+
296
+ if missing_commands:
297
+ commands_to_append = ["\n\n# --- Commands from Agent Starter Pack ---\n\n"]
298
+ for command in sorted(missing_commands):
299
+ command_block_match = re.search(
300
+ rf"^{command}:.*?(?=\n\n(?:^#.*\n)*?^[a-zA-Z0-9_-]+:|" + r"\Z)",
301
+ rendered_base_makefile,
302
+ re.MULTILINE | re.DOTALL,
303
+ )
304
+ if command_block_match:
305
+ commands_to_append.append(command_block_match.group(0))
306
+ commands_to_append.append("\n\n")
307
+
308
+ final_makefile_content = rendered_remote_makefile + "".join(
309
+ commands_to_append
310
+ )
311
+ else:
312
+ final_makefile_content = rendered_remote_makefile
313
+ elif rendered_remote_makefile:
314
+ final_makefile_content = rendered_remote_makefile
315
+ else:
316
+ final_makefile_content = rendered_base_makefile
317
+
318
+ # Write the final merged Makefile
319
+ with open(final_destination / "Makefile", "w", encoding="utf-8") as f:
320
+ f.write(final_makefile_content)
321
+ logging.debug("Rendered and merged Makefile written to final destination.")
src/cli/utils/template.py CHANGED
@@ -31,6 +31,7 @@ from src.cli.utils.version import get_current_version
31
31
  from .datastores import DATASTORES
32
32
  from .remote_template import (
33
33
  get_base_template_name,
34
+ render_and_merge_makefiles,
34
35
  )
35
36
 
36
37
  ADK_FILES = ["app/__init__.py"]
@@ -47,7 +48,7 @@ class TemplateConfig:
47
48
  def from_file(cls, config_path: pathlib.Path) -> "TemplateConfig":
48
49
  """Load template config from file with validation"""
49
50
  try:
50
- with open(config_path) as f:
51
+ with open(config_path, encoding="utf-8") as f:
51
52
  data = yaml.safe_load(f)
52
53
 
53
54
  if not isinstance(data, dict):
@@ -100,7 +101,7 @@ def get_available_agents(deployment_target: str | None = None) -> dict:
100
101
  template_config_path = agent_dir / ".template" / "templateconfig.yaml"
101
102
  if template_config_path.exists():
102
103
  try:
103
- with open(template_config_path) as f:
104
+ with open(template_config_path, encoding="utf-8") as f:
104
105
  config = yaml.safe_load(f)
105
106
  agent_name = agent_dir.name
106
107
 
@@ -149,7 +150,7 @@ def load_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
149
150
  return {}
150
151
 
151
152
  try:
152
- with open(config_file) as f:
153
+ with open(config_file, encoding="utf-8") as f:
153
154
  config = yaml.safe_load(f)
154
155
  return config if config else {}
155
156
  except Exception as e:
@@ -229,6 +230,10 @@ def prompt_session_type_selection() -> str:
229
230
  "display_name": "AlloyDB",
230
231
  "description": "Use AlloyDB for session management. Comes with terraform resources for deployment.",
231
232
  },
233
+ "agent_engine": {
234
+ "display_name": "Vertex AI Agent Engine",
235
+ "description": "Managed session service that automatically handles conversation history",
236
+ },
232
237
  }
233
238
 
234
239
  console.print("\n> Please select a session type:")
@@ -356,6 +361,36 @@ def prompt_datastore_selection(
356
361
  return datastore_type
357
362
 
358
363
 
364
+ def prompt_cicd_runner_selection() -> str:
365
+ """Ask user to select a CI/CD runner."""
366
+ console = Console()
367
+
368
+ cicd_runners = {
369
+ "google_cloud_build": {
370
+ "display_name": "Google Cloud Build",
371
+ "description": "Fully managed CI/CD, deeply integrated with GCP for fast, consistent builds and deployments.",
372
+ },
373
+ "github_actions": {
374
+ "display_name": "GitHub Actions",
375
+ "description": "GitHub Actions: CI/CD with secure workload identity federation directly in GitHub.",
376
+ },
377
+ }
378
+
379
+ console.print("\n> Please select a CI/CD runner:")
380
+ for idx, (_key, info) in enumerate(cicd_runners.items(), 1):
381
+ console.print(
382
+ f"{idx}. [bold]{info['display_name']}[/] - [dim]{info['description']}[/]"
383
+ )
384
+
385
+ choice = IntPrompt.ask(
386
+ "\nEnter the number of your CI/CD runner choice",
387
+ default=1,
388
+ show_default=True,
389
+ )
390
+
391
+ return list(cicd_runners.keys())[choice - 1]
392
+
393
+
359
394
  def get_template_path(agent_name: str, debug: bool = False) -> pathlib.Path:
360
395
  """Get the absolute path to the agent template directory."""
361
396
  current_dir = pathlib.Path(__file__).parent.parent.parent.parent
@@ -403,6 +438,7 @@ def process_template(
403
438
  template_dir: pathlib.Path,
404
439
  project_name: str,
405
440
  deployment_target: str | None = None,
441
+ cicd_runner: str | None = None,
406
442
  include_data_ingestion: bool = False,
407
443
  datastore: str | None = None,
408
444
  session_type: str | None = None,
@@ -417,6 +453,7 @@ def process_template(
417
453
  template_dir: Directory containing the template files
418
454
  project_name: Name of the project to create
419
455
  deployment_target: Optional deployment target (agent_engine or cloud_run)
456
+ cicd_runner: Optional CI/CD runner to use
420
457
  include_data_ingestion: Whether to include data pipeline components
421
458
  datastore: Optional datastore type for data ingestion
422
459
  session_type: Optional session type for cloud_run deployment
@@ -584,13 +621,13 @@ def process_template(
584
621
  / "docs"
585
622
  / "adk-cheatsheet.md"
586
623
  )
587
- with open(adk_cheatsheet_path) as f:
624
+ with open(adk_cheatsheet_path, encoding="utf-8") as f:
588
625
  adk_cheatsheet_content = f.read()
589
626
 
590
627
  llm_txt_path = (
591
628
  pathlib.Path(__file__).parent.parent.parent.parent / "llm.txt"
592
629
  )
593
- with open(llm_txt_path) as f:
630
+ with open(llm_txt_path, encoding="utf-8") as f:
594
631
  llm_txt_content = f.read()
595
632
 
596
633
  cookiecutter_config = {
@@ -604,6 +641,7 @@ def process_template(
604
641
  "settings": settings,
605
642
  "tags": tags,
606
643
  "deployment_target": deployment_target or "",
644
+ "cicd_runner": cicd_runner or "google_cloud_build",
607
645
  "session_type": session_type or "",
608
646
  "frontend_type": frontend_type,
609
647
  "extra_dependencies": [extra_deps],
@@ -622,12 +660,15 @@ def process_template(
622
660
  ".pytest_cache/*",
623
661
  ".venv/*",
624
662
  "*templates.py", # Don't render templates files
663
+ "Makefile", # Don't render Makefile - handled by render_and_merge_makefiles
625
664
  # Don't render agent.py unless it's agentic_rag
626
665
  "app/agent.py" if agent_name != "agentic_rag" else "",
627
666
  ],
628
667
  }
629
668
 
630
- with open(cookiecutter_template / "cookiecutter.json", "w") as f:
669
+ with open(
670
+ cookiecutter_template / "cookiecutter.json", "w", encoding="utf-8"
671
+ ) as f:
631
672
  json.dump(cookiecutter_config, f, indent=4)
632
673
 
633
674
  logging.debug(f"Template structure created at {cookiecutter_template}")
@@ -639,6 +680,7 @@ def process_template(
639
680
  cookiecutter(
640
681
  str(cookiecutter_template),
641
682
  no_input=True,
683
+ overwrite_if_exists=True,
642
684
  extra_context={
643
685
  "project_name": project_name,
644
686
  "agent_name": agent_name,
@@ -658,6 +700,16 @@ def process_template(
658
700
  shutil.copytree(output_dir, final_destination, dirs_exist_ok=True)
659
701
  logging.debug(f"Project successfully created at {final_destination}")
660
702
 
703
+ # Render and merge Makefiles.
704
+ # If it's a local template, remote_template_path will be None,
705
+ # and only the base Makefile will be rendered.
706
+ render_and_merge_makefiles(
707
+ base_template_path=base_template_path,
708
+ final_destination=final_destination,
709
+ cookiecutter_config=cookiecutter_config,
710
+ remote_template_path=remote_template_path,
711
+ )
712
+
661
713
  # Delete appropriate files based on ADK tag
662
714
  if "adk" in tags:
663
715
  files_to_delete = [final_destination / f for f in NON_ADK_FILES]
@@ -669,6 +721,27 @@ def process_template(
669
721
  file_path.unlink()
670
722
  logging.debug(f"Deleted {file_path}")
671
723
 
724
+ # Clean up unused_* files and directories created by conditional templates
725
+ import glob
726
+
727
+ unused_patterns = [
728
+ final_destination / "unused_*",
729
+ final_destination / "**" / "unused_*",
730
+ ]
731
+
732
+ for pattern in unused_patterns:
733
+ for unused_path_str in glob.glob(str(pattern), recursive=True):
734
+ unused_path = pathlib.Path(unused_path_str)
735
+ if unused_path.exists():
736
+ if unused_path.is_dir():
737
+ shutil.rmtree(unused_path)
738
+ logging.debug(
739
+ f"Deleted unused directory: {unused_path}"
740
+ )
741
+ else:
742
+ unused_path.unlink()
743
+ logging.debug(f"Deleted unused file: {unused_path}")
744
+
672
745
  # Handle pyproject.toml and uv.lock files
673
746
  if is_remote and remote_template_path:
674
747
  # For remote templates, use their pyproject.toml and uv.lock if they exist
@@ -684,19 +757,6 @@ def process_template(
684
757
  if remote_uv_lock.exists():
685
758
  shutil.copy2(remote_uv_lock, final_destination / "uv.lock")
686
759
  logging.debug("Used uv.lock from remote template")
687
- elif deployment_target:
688
- # Fallback to base template lock file
689
- base_template_name = get_base_template_name(remote_config or {})
690
- lock_path = (
691
- pathlib.Path(__file__).parent.parent.parent.parent
692
- / "src"
693
- / "resources"
694
- / "locks"
695
- / f"uv-{base_template_name}-{deployment_target}.lock"
696
- )
697
- if lock_path.exists():
698
- shutil.copy2(lock_path, final_destination / "uv.lock")
699
- logging.debug(f"Used fallback lock file from {lock_path}")
700
760
  elif deployment_target:
701
761
  # For local templates, use the existing logic
702
762
  lock_path = (
@@ -20,6 +20,9 @@ from google.adk.cli.fast_api import get_fast_api_app
20
20
  from google.cloud import logging as google_cloud_logging
21
21
  from opentelemetry import trace
22
22
  from opentelemetry.sdk.trace import TracerProvider, export
23
+ {%- if cookiecutter.session_type == "agent_engine" %}
24
+ from vertexai import agent_engines
25
+ {%- endif %}
23
26
 
24
27
  from app.utils.gcs import create_bucket_if_not_exists
25
28
  from app.utils.tracing import CloudTraceLoggingSpanExporter
@@ -55,6 +58,22 @@ db_host = os.environ.get("DB_HOST")
55
58
  session_service_uri = None
56
59
  if db_host and db_pass:
57
60
  session_service_uri = f"postgresql://{db_user}:{db_pass}@{db_host}:5432/{db_name}"
61
+ {%- elif cookiecutter.session_type == "agent_engine" %}
62
+ # Agent Engine session configuration
63
+ # Use environment variable for agent name, default to project name
64
+ agent_name = os.environ.get("AGENT_ENGINE_SESSION_NAME", "{{cookiecutter.project_name}}")
65
+
66
+ # Check if an agent with this name already exists
67
+ existing_agents = list(agent_engines.list(filter=f"display_name={agent_name}"))
68
+
69
+ if existing_agents:
70
+ # Use the existing agent
71
+ agent_engine = existing_agents[0]
72
+ else:
73
+ # Create a new agent if none exists
74
+ agent_engine = agent_engines.create(display_name=agent_name)
75
+
76
+ session_service_uri = f"agentengine://{agent_engine.resource_name}"
58
77
  {%- else %}
59
78
  # In-memory session configuration - no persistent storage
60
79
  session_service_uri = None
@@ -63,9 +63,10 @@ resource "google_service_networking_connection" "vpc_connection" {
63
63
 
64
64
  # AlloyDB Cluster
65
65
  resource "google_alloydb_cluster" "session_db_cluster" {
66
- project = var.dev_project_id
67
- cluster_id = "${var.project_name}-alloydb-cluster"
68
- location = var.region
66
+ project = var.dev_project_id
67
+ cluster_id = "${var.project_name}-alloydb-cluster"
68
+ location = var.region
69
+ deletion_policy = "FORCE"
69
70
 
70
71
  network_config {
71
72
  network = google_compute_network.default.id
@@ -75,9 +75,10 @@ resource "google_service_networking_connection" "vpc_connection" {
75
75
  resource "google_alloydb_cluster" "session_db_cluster" {
76
76
  for_each = local.deploy_project_ids
77
77
 
78
- project = local.deploy_project_ids[each.key]
79
- cluster_id = "${var.project_name}-alloydb-cluster"
80
- location = var.region
78
+ project = local.deploy_project_ids[each.key]
79
+ cluster_id = "${var.project_name}-alloydb-cluster"
80
+ location = var.region
81
+ deletion_policy = "FORCE"
81
82
 
82
83
  network_config {
83
84
  network = google_compute_network.default[each.key].id
@@ -62,6 +62,10 @@ def start_server() -> subprocess.Popen[str]:
62
62
  ]
63
63
  env = os.environ.copy()
64
64
  env["INTEGRATION_TEST"] = "TRUE"
65
+ {%- if cookiecutter.session_type == "agent_engine" %}
66
+ # Set test-specific agent engine session name
67
+ env["AGENT_ENGINE_SESSION_NAME"] = "test-{{cookiecutter.project_name}}"
68
+ {%- endif %}
65
69
  process = subprocess.Popen(
66
70
  command,
67
71
  stdout=subprocess.PIPE,
@@ -250,3 +254,34 @@ def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
250
254
  FEEDBACK_URL, json=feedback_data, headers=HEADERS, timeout=10
251
255
  )
252
256
  assert response.status_code == 200
257
+ {%- if cookiecutter.session_type == "agent_engine" %}
258
+
259
+
260
+ @pytest.fixture(scope="session", autouse=True)
261
+ def cleanup_agent_engine_sessions() -> None:
262
+ """Cleanup agent engine sessions created during tests."""
263
+ yield # Run tests first
264
+
265
+ # Cleanup after tests complete
266
+ from vertexai import agent_engines
267
+
268
+ try:
269
+ # Use same environment variable as server, default to project name
270
+ agent_name = os.environ.get(
271
+ "AGENT_ENGINE_SESSION_NAME", "{{cookiecutter.project_name}}"
272
+ )
273
+
274
+ # Find and delete agent engines with this name
275
+ existing_agents = list(agent_engines.list(filter=f"display_name={agent_name}"))
276
+
277
+ for agent_engine in existing_agents:
278
+ try:
279
+ agent_engines.delete(resource_name=agent_engine.name)
280
+ logger.info(f"Cleaned up agent engine: {agent_engine.name}")
281
+ except Exception as e:
282
+ logger.warning(
283
+ f"Failed to cleanup agent engine {agent_engine.name}: {e}"
284
+ )
285
+ except Exception as e:
286
+ logger.warning(f"Failed to cleanup agent engine sessions: {e}")
287
+ {%- endif %}
@@ -41,7 +41,7 @@ Comprehensive CSV and HTML reports detailing the load test performance will be g
41
41
 
42
42
  ## Remote Load Testing (Targeting Cloud Run)
43
43
 
44
- This framework also supports load testing against remote targets, such as a staging Cloud Run instance. This process is seamlessly integrated into the Continuous Delivery pipeline via Cloud Build, as defined in the [pipeline file](cicd/cd/staging.yaml).
44
+ This framework also supports load testing against remote targets, such as a staging Cloud Run instance. This process is seamlessly integrated into the Continuous Delivery (CD) pipeline.
45
45
 
46
46
  **Prerequisites:**
47
47
 
@@ -6452,16 +6452,16 @@
6452
6452
  }
6453
6453
  },
6454
6454
  "node_modules/compression": {
6455
- "version": "1.7.5",
6456
- "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
6457
- "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
6455
+ "version": "1.8.1",
6456
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
6457
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
6458
6458
  "license": "MIT",
6459
6459
  "dependencies": {
6460
6460
  "bytes": "3.1.2",
6461
6461
  "compressible": "~2.0.18",
6462
6462
  "debug": "2.6.9",
6463
6463
  "negotiator": "~0.6.4",
6464
- "on-headers": "~1.0.2",
6464
+ "on-headers": "~1.1.0",
6465
6465
  "safe-buffer": "5.2.1",
6466
6466
  "vary": "~1.1.2"
6467
6467
  },
@@ -8038,14 +8038,15 @@
8038
8038
  }
8039
8039
  },
8040
8040
  "node_modules/es-set-tostringtag": {
8041
- "version": "2.0.3",
8042
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
8043
- "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
8041
+ "version": "2.1.0",
8042
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
8043
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
8044
8044
  "license": "MIT",
8045
8045
  "dependencies": {
8046
- "get-intrinsic": "^1.2.4",
8046
+ "es-errors": "^1.3.0",
8047
+ "get-intrinsic": "^1.2.6",
8047
8048
  "has-tostringtag": "^1.0.2",
8048
- "hasown": "^2.0.1"
8049
+ "hasown": "^2.0.2"
8049
8050
  },
8050
8051
  "engines": {
8051
8052
  "node": ">= 0.4"
@@ -9335,14 +9336,16 @@
9335
9336
  }
9336
9337
  },
9337
9338
  "node_modules/form-data": {
9338
- "version": "3.0.2",
9339
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz",
9340
- "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==",
9339
+ "version": "3.0.4",
9340
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
9341
+ "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
9341
9342
  "license": "MIT",
9342
9343
  "dependencies": {
9343
9344
  "asynckit": "^0.4.0",
9344
9345
  "combined-stream": "^1.0.8",
9345
- "mime-types": "^2.1.12"
9346
+ "es-set-tostringtag": "^2.1.0",
9347
+ "hasown": "^2.0.2",
9348
+ "mime-types": "^2.1.35"
9346
9349
  },
9347
9350
  "engines": {
9348
9351
  "node": ">= 6"
@@ -12916,9 +12919,9 @@
12916
12919
  }
12917
12920
  },
12918
12921
  "node_modules/on-headers": {
12919
- "version": "1.0.2",
12920
- "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
12921
- "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
12922
+ "version": "1.1.0",
12923
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
12924
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
12922
12925
  "license": "MIT",
12923
12926
  "engines": {
12924
12927
  "node": ">= 0.8"
@@ -31,7 +31,7 @@ DEFAULT_BASE_URL = "http://localhost:8000/"
31
31
 
32
32
  DEFAULT_REMOTE_AGENT_ENGINE_ID = "N/A"
33
33
  if os.path.exists("deployment_metadata.json"):
34
- with open("deployment_metadata.json") as f:
34
+ with open("deployment_metadata.json", encoding="utf-8") as f:
35
35
  DEFAULT_REMOTE_AGENT_ENGINE_ID = json.load(f)["remote_agent_engine_id"]
36
36
  DEFAULT_AGENT_CALLABLE_PATH = "app.agent_engine_app.AgentEngineApp"
37
37
 
@@ -56,7 +56,7 @@ def save_chat(st: Any) -> None:
56
56
  if len(messages) > 0:
57
57
  session["messages"] = sanitize_messages(session["messages"])
58
58
  filename = f"{session_id}.yaml"
59
- with open(Path(SAVED_CHAT_PATH) / filename, "w") as file:
59
+ with open(Path(SAVED_CHAT_PATH) / filename, "w", encoding="utf-8") as file:
60
60
  yaml.dump(
61
61
  [session],
62
62
  file,