agent-starter-pack 0.10.1__py3-none-any.whl → 0.11.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 (43) hide show
  1. {agent_starter_pack-0.10.1.dist-info → agent_starter_pack-0.11.0.dist-info}/METADATA +2 -2
  2. {agent_starter_pack-0.10.1.dist-info → agent_starter_pack-0.11.0.dist-info}/RECORD +43 -42
  3. agents/crewai_coding_crew/.template/templateconfig.yaml +2 -2
  4. agents/crewai_coding_crew/tests/integration/test_agent.py +1 -1
  5. agents/langgraph_base_react/.template/templateconfig.yaml +1 -1
  6. agents/langgraph_base_react/tests/integration/test_agent.py +1 -1
  7. src/base_template/deployment/terraform/dev/iam.tf +12 -11
  8. src/base_template/deployment/terraform/dev/variables.tf +2 -7
  9. src/base_template/deployment/terraform/github.tf +14 -0
  10. src/base_template/deployment/terraform/iam.tf +10 -7
  11. src/base_template/deployment/terraform/service_accounts.tf +4 -5
  12. src/base_template/deployment/terraform/variables.tf +2 -7
  13. src/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} +4 -2
  14. src/base_template/pyproject.toml +2 -2
  15. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +1 -0
  16. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +1 -0
  17. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +1 -0
  18. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +1 -0
  19. src/cli/commands/create.py +202 -100
  20. src/cli/commands/enhance.py +248 -0
  21. src/cli/commands/list.py +23 -11
  22. src/cli/main.py +2 -0
  23. src/cli/utils/logging.py +40 -0
  24. src/cli/utils/remote_template.py +55 -16
  25. src/cli/utils/template.py +212 -94
  26. src/deployment_targets/agent_engine/app/agent_engine_app.py +8 -0
  27. src/deployment_targets/cloud_run/app/server.py +1 -1
  28. src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +1 -1
  29. src/deployment_targets/cloud_run/deployment/terraform/service.tf +2 -2
  30. src/resources/locks/uv-adk_base-agent_engine.lock +312 -312
  31. src/resources/locks/uv-adk_base-cloud_run.lock +403 -404
  32. src/resources/locks/uv-adk_gemini_fullstack-agent_engine.lock +312 -312
  33. src/resources/locks/uv-adk_gemini_fullstack-cloud_run.lock +403 -404
  34. src/resources/locks/uv-agentic_rag-agent_engine.lock +371 -371
  35. src/resources/locks/uv-agentic_rag-cloud_run.lock +477 -478
  36. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +661 -591
  37. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +868 -760
  38. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +496 -446
  39. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +639 -565
  40. src/resources/locks/uv-live_api-cloud_run.lock +584 -510
  41. {agent_starter_pack-0.10.1.dist-info → agent_starter_pack-0.11.0.dist-info}/WHEEL +0 -0
  42. {agent_starter_pack-0.10.1.dist-info → agent_starter_pack-0.11.0.dist-info}/entry_points.txt +0 -0
  43. {agent_starter_pack-0.10.1.dist-info → agent_starter_pack-0.11.0.dist-info}/licenses/LICENSE +0 -0
src/cli/commands/list.py CHANGED
@@ -16,7 +16,7 @@ import logging
16
16
  import pathlib
17
17
 
18
18
  import click
19
- import yaml
19
+ import tomllib
20
20
  from rich.console import Console
21
21
  from rich.table import Table
22
22
 
@@ -42,25 +42,37 @@ def display_agents_from_path(base_path: pathlib.Path, source_name: str) -> None:
42
42
  return
43
43
 
44
44
  found_agents = False
45
- # Search for templateconfig.yaml files to identify agents
46
- for config_path in sorted(base_path.glob("**/templateconfig.yaml")):
45
+ # Search for pyproject.toml files to identify agents (explicit opt-in)
46
+ for config_path in sorted(base_path.glob("**/pyproject.toml")):
47
47
  try:
48
- with open(config_path, encoding="utf-8") as f:
49
- config = yaml.safe_load(f)
48
+ with open(config_path, "rb") as f:
49
+ pyproject_data = tomllib.load(f)
50
50
 
51
- agent_name = config.get("name", config_path.parent.parent.name)
52
- description = config.get("description", "No description.")
51
+ config = pyproject_data.get("tool", {}).get("agent-starter-pack", {})
52
+
53
+ # Skip pyproject.toml files that don't have agent-starter-pack config
54
+ if not config:
55
+ continue
56
+
57
+ template_root = config_path.parent
58
+
59
+ # Use fallbacks to [project] section if needed
60
+ project_info = pyproject_data.get("project", {})
61
+ agent_name = (
62
+ config.get("name") or project_info.get("name") or template_root.name
63
+ )
64
+ description = (
65
+ config.get("description") or project_info.get("description") or ""
66
+ )
53
67
 
54
68
  # Display the agent's path relative to the scanned directory
55
- relative_path = config_path.parent.parent.relative_to(base_path)
69
+ relative_path = template_root.relative_to(base_path)
56
70
 
57
71
  table.add_row(agent_name, f"/{relative_path}", description)
58
72
  found_agents = True
59
73
 
60
74
  except Exception as e:
61
- logging.warning(
62
- f"Could not load agent from {config_path.parent.parent}: {e}"
63
- )
75
+ logging.warning(f"Could not load agent from {config_path.parent}: {e}")
64
76
 
65
77
  if not found_agents:
66
78
  console.print(f"No agents found in {source_name}", style="yellow")
src/cli/main.py CHANGED
@@ -18,6 +18,7 @@ import click
18
18
  from rich.console import Console
19
19
 
20
20
  from .commands.create import create
21
+ from .commands.enhance import enhance
21
22
  from .commands.list import list_agents
22
23
  from .commands.setup_cicd import setup_cicd
23
24
  from .utils import display_update_message
@@ -53,6 +54,7 @@ def cli() -> None:
53
54
 
54
55
  # Register commands
55
56
  cli.add_command(create)
57
+ cli.add_command(enhance)
56
58
  cli.add_command(setup_cicd)
57
59
  cli.add_command(list_agents, name="list")
58
60
 
src/cli/utils/logging.py CHANGED
@@ -24,6 +24,46 @@ console = Console()
24
24
  F = TypeVar("F", bound=Callable[..., Any])
25
25
 
26
26
 
27
+ def display_welcome_banner(
28
+ agent: str | None = None, enhance_mode: bool = False
29
+ ) -> None:
30
+ """Display the Agent Starter Pack welcome banner.
31
+
32
+ Args:
33
+ agent: Optional agent specification to customize the welcome message
34
+ enhance_mode: Whether this is for enhancement mode
35
+ """
36
+ if enhance_mode:
37
+ console.print(
38
+ "\n=== Google Cloud Agent Starter Pack 🚀===",
39
+ style="bold blue",
40
+ )
41
+ console.print(
42
+ "Enhancing your existing project with production-ready agent capabilities!\n",
43
+ style="green",
44
+ )
45
+ elif agent and agent.startswith("adk@"):
46
+ console.print(
47
+ "\n=== Welcome to [link=https://github.com/google/adk-samples]google/adk-samples[/link]! ✨ ===",
48
+ style="bold blue",
49
+ )
50
+ console.print(
51
+ "Powered by [link=https://goo.gle/agent-starter-pack]Google Cloud - Agent Starter Pack [/link]\n",
52
+ )
53
+ console.print(
54
+ "This tool will help you create an end-to-end production-ready AI agent in Google Cloud!\n"
55
+ )
56
+ else:
57
+ console.print(
58
+ "\n=== Google Cloud Agent Starter Pack 🚀===",
59
+ style="bold blue",
60
+ )
61
+ console.print("Welcome to the Agent Starter Pack!")
62
+ console.print(
63
+ "This tool will help you create an end-to-end production-ready AI agent in Google Cloud!\n"
64
+ )
65
+
66
+
27
67
  def handle_cli_error(f: F) -> F:
28
68
  """Decorator to handle CLI errors gracefully.
29
69
 
@@ -22,7 +22,7 @@ import tempfile
22
22
  from dataclasses import dataclass
23
23
  from typing import Any
24
24
 
25
- import yaml
25
+ import tomllib
26
26
  from jinja2 import Environment
27
27
 
28
28
 
@@ -183,27 +183,66 @@ def fetch_remote_template(
183
183
  ) from e
184
184
 
185
185
 
186
- def load_remote_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
187
- """Load template configuration from remote template.
186
+ def load_remote_template_config(
187
+ template_dir: pathlib.Path, cli_overrides: dict[str, Any] | None = None
188
+ ) -> dict[str, Any]:
189
+ """Load template configuration from remote template's pyproject.toml with CLI overrides.
190
+
191
+ Loads configuration from [tool.agent-starter-pack] section with fallbacks
192
+ to [project] section for name and description if not specified. CLI overrides
193
+ take precedence over all other sources.
188
194
 
189
195
  Args:
190
196
  template_dir: Path to template directory
197
+ cli_overrides: Configuration overrides from CLI (takes highest precedence)
191
198
 
192
199
  Returns:
193
- Template configuration dictionary
200
+ Template configuration dictionary with merged sources
194
201
  """
195
- config_path = template_dir / ".template" / "templateconfig.yaml"
196
-
197
- if not config_path.exists():
198
- return {}
199
-
200
- try:
201
- with open(config_path, encoding="utf-8") as f:
202
- config = yaml.safe_load(f)
203
- return config if config else {}
204
- except Exception as e:
205
- logging.error(f"Error loading remote template config: {e}")
206
- return {}
202
+ config = {}
203
+
204
+ # Start with defaults
205
+ defaults = {
206
+ "base_template": "adk_base",
207
+ "name": template_dir.name,
208
+ "description": "",
209
+ }
210
+ config.update(defaults)
211
+
212
+ # Load from pyproject.toml if it exists
213
+ pyproject_path = template_dir / "pyproject.toml"
214
+ if pyproject_path.exists():
215
+ try:
216
+ with open(pyproject_path, "rb") as f:
217
+ pyproject_data = tomllib.load(f)
218
+
219
+ # Extract the agent-starter-pack configuration
220
+ toml_config = pyproject_data.get("tool", {}).get("agent-starter-pack", {})
221
+
222
+ # Fallback to [project] fields if not specified in agent-starter-pack section
223
+ project_info = pyproject_data.get("project", {})
224
+
225
+ # Apply pyproject.toml configuration (overrides defaults)
226
+ if toml_config:
227
+ config.update(toml_config)
228
+
229
+ # Apply [project] fallbacks if not already set
230
+ if "name" not in toml_config and "name" in project_info:
231
+ config["name"] = project_info["name"]
232
+
233
+ if "description" not in toml_config and "description" in project_info:
234
+ config["description"] = project_info["description"]
235
+
236
+ logging.debug(f"Loaded template config from {pyproject_path}")
237
+ except Exception as e:
238
+ logging.error(f"Error loading pyproject.toml config: {e}")
239
+
240
+ # Apply CLI overrides (highest precedence) using deep merge
241
+ if cli_overrides:
242
+ config = merge_template_configs(config, cli_overrides)
243
+ logging.debug(f"Applied CLI overrides: {cli_overrides}")
244
+
245
+ return config
207
246
 
208
247
 
209
248
  def get_base_template_name(config: dict[str, Any]) -> str:
src/cli/utils/template.py CHANGED
@@ -445,6 +445,7 @@ def process_template(
445
445
  output_dir: pathlib.Path | None = None,
446
446
  remote_template_path: pathlib.Path | None = None,
447
447
  remote_config: dict[str, Any] | None = None,
448
+ in_folder: bool = False,
448
449
  ) -> None:
449
450
  """Process the template directory and create a new project.
450
451
 
@@ -460,6 +461,7 @@ def process_template(
460
461
  output_dir: Optional output directory path, defaults to current directory
461
462
  remote_template_path: Optional path to remote template for overlay
462
463
  remote_config: Optional remote template configuration
464
+ in_folder: Whether to template directly into the output directory instead of creating a subdirectory
463
465
  """
464
466
  logging.debug(f"Processing template from {template_dir}")
465
467
  logging.debug(f"Project name: {project_name}")
@@ -689,112 +691,228 @@ def process_template(
689
691
  logging.debug("Template processing completed successfully")
690
692
 
691
693
  # Move the generated project to the final destination
692
- output_dir = temp_path / project_name
693
- final_destination = destination_dir / project_name
694
-
695
- logging.debug(f"Moving project from {output_dir} to {final_destination}")
696
-
697
- if output_dir.exists():
698
- if final_destination.exists():
699
- shutil.rmtree(final_destination)
700
- shutil.copytree(output_dir, final_destination, dirs_exist_ok=True)
701
- logging.debug(f"Project successfully created at {final_destination}")
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,
694
+ generated_project_dir = temp_path / project_name
695
+
696
+ if in_folder:
697
+ # For in-folder mode, copy files directly to the destination directory
698
+ final_destination = destination_dir
699
+ logging.debug(
700
+ f"In-folder mode: copying files from {generated_project_dir} to {final_destination}"
711
701
  )
712
702
 
713
- # Delete appropriate files based on ADK tag
714
- if "adk" in tags:
715
- files_to_delete = [final_destination / f for f in NON_ADK_FILES]
716
- else:
717
- files_to_delete = [final_destination / f for f in ADK_FILES]
718
-
719
- for file_path in files_to_delete:
720
- if file_path.exists():
721
- file_path.unlink()
722
- logging.debug(f"Deleted {file_path}")
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)
703
+ if generated_project_dir.exists():
704
+ # Copy all files from generated project to destination directory
705
+ for item in generated_project_dir.iterdir():
706
+ dest_item = final_destination / item.name
707
+
708
+ # Special handling for README files - always preserve existing README
709
+ # Special handling for pyproject.toml files - only preserve for in-folder updates
710
+ should_preserve_file = item.name.lower().startswith(
711
+ "readme"
712
+ ) or (item.name == "pyproject.toml" and in_folder)
713
+ if (
714
+ should_preserve_file
715
+ and (final_destination / item.name).exists()
716
+ ):
717
+ # The existing file stays, use base template file with starter_pack prefix
718
+ base_name = item.stem
719
+ extension = item.suffix
720
+ dest_item = (
721
+ final_destination
722
+ / f"starter_pack_{base_name}{extension}"
723
+ )
724
+
725
+ # Try to use base template file instead of templated file
726
+ base_file = base_template_path / item.name
727
+ if base_file.exists():
738
728
  logging.debug(
739
- f"Deleted unused directory: {unused_path}"
729
+ f"{item.name} conflict: preserving existing {item.name}, using base template {item.name} as starter_pack_{base_name}{extension}"
740
730
  )
731
+ # Process the base template file through cookiecutter
732
+ try:
733
+ import tempfile as tmp_module
734
+
735
+ with (
736
+ tmp_module.TemporaryDirectory() as temp_file_dir
737
+ ):
738
+ temp_file_path = pathlib.Path(temp_file_dir)
739
+
740
+ # Create a minimal cookiecutter structure for just the file
741
+ file_template_dir = (
742
+ temp_file_path / "file_template"
743
+ )
744
+ file_template_dir.mkdir()
745
+ file_project_dir = (
746
+ file_template_dir
747
+ / "{{cookiecutter.project_name}}"
748
+ )
749
+ file_project_dir.mkdir()
750
+
751
+ # Copy base file to template structure
752
+ shutil.copy2(
753
+ base_file, file_project_dir / item.name
754
+ )
755
+
756
+ # Create cookiecutter.json with same config as main template
757
+ with open(
758
+ file_template_dir / "cookiecutter.json",
759
+ "w",
760
+ encoding="utf-8",
761
+ ) as f:
762
+ json.dump(cookiecutter_config, f, indent=4)
763
+
764
+ # Process the file template
765
+ cookiecutter(
766
+ str(file_template_dir),
767
+ no_input=True,
768
+ overwrite_if_exists=True,
769
+ output_dir=str(temp_file_path),
770
+ extra_context={
771
+ "project_name": project_name,
772
+ "agent_name": agent_name,
773
+ },
774
+ )
775
+
776
+ # Copy the processed file
777
+ processed_file = (
778
+ temp_file_path / project_name / item.name
779
+ )
780
+ if processed_file.exists():
781
+ shutil.copy2(processed_file, dest_item)
782
+ else:
783
+ # Fallback to original behavior if processing fails
784
+ shutil.copy2(item, dest_item)
785
+
786
+ except Exception as e:
787
+ logging.warning(
788
+ f"Failed to process base template {item.name}: {e}. Using templated {item.name} instead."
789
+ )
790
+ shutil.copy2(item, dest_item)
741
791
  else:
742
- unused_path.unlink()
743
- logging.debug(f"Deleted unused file: {unused_path}")
744
-
745
- # Handle pyproject.toml and uv.lock files
746
- if is_remote and remote_template_path:
747
- # For remote templates, use their pyproject.toml and uv.lock if they exist
748
- remote_pyproject = remote_template_path / "pyproject.toml"
749
- remote_uv_lock = remote_template_path / "uv.lock"
750
-
751
- if remote_pyproject.exists():
752
- shutil.copy2(
753
- remote_pyproject, final_destination / "pyproject.toml"
754
- )
755
- logging.debug("Used pyproject.toml from remote template")
756
-
757
- if remote_uv_lock.exists():
758
- shutil.copy2(remote_uv_lock, final_destination / "uv.lock")
759
- logging.debug("Used uv.lock from remote template")
760
- elif deployment_target:
761
- # For local templates, use the existing logic
762
- lock_path = (
763
- pathlib.Path(__file__).parent.parent.parent.parent
764
- / "src"
765
- / "resources"
766
- / "locks"
767
- / f"uv-{agent_name}-{deployment_target}.lock"
768
- )
769
- logging.debug(f"Looking for lock file at: {lock_path}")
770
- logging.debug(f"Lock file exists: {lock_path.exists()}")
771
- if not lock_path.exists():
772
- raise FileNotFoundError(f"Lock file not found: {lock_path}")
773
- # Copy and rename to uv.lock in the project directory
774
- shutil.copy2(lock_path, final_destination / "uv.lock")
792
+ # Fallback to original behavior if base file doesn't exist
793
+ logging.debug(
794
+ f"{item.name} conflict: preserving existing {item.name}, saving templated {item.name} as starter_pack_{base_name}{extension}"
795
+ )
796
+ shutil.copy2(item, dest_item)
797
+ else:
798
+ # Normal file copying
799
+ if item.is_dir():
800
+ if dest_item.exists():
801
+ shutil.rmtree(dest_item)
802
+ shutil.copytree(item, dest_item, dirs_exist_ok=True)
803
+ else:
804
+ shutil.copy2(item, dest_item)
775
805
  logging.debug(
776
- f"Copied lock file from {lock_path} to {final_destination}/uv.lock"
806
+ f"Project files successfully copied to {final_destination}"
777
807
  )
808
+ else:
809
+ # Standard mode: create project subdirectory
810
+ final_destination = destination_dir / project_name
811
+ logging.debug(
812
+ f"Standard mode: moving project from {generated_project_dir} to {final_destination}"
813
+ )
778
814
 
779
- # Replace cookiecutter project name with actual project name in lock file
780
- lock_file_path = final_destination / "uv.lock"
781
- with open(lock_file_path, "r+", encoding="utf-8") as f:
782
- content = f.read()
783
- f.seek(0)
784
- f.write(
785
- content.replace(
786
- "{{cookiecutter.project_name}}", project_name
787
- )
788
- )
789
- f.truncate()
815
+ if generated_project_dir.exists():
816
+ if final_destination.exists():
817
+ shutil.rmtree(final_destination)
818
+ shutil.copytree(
819
+ generated_project_dir, final_destination, dirs_exist_ok=True
820
+ )
790
821
  logging.debug(
791
- f"Updated project name in lock file at {lock_file_path}"
822
+ f"Project successfully created at {final_destination}"
792
823
  )
793
- else:
794
- logging.error(f"Generated project directory not found at {output_dir}")
824
+
825
+ # Always check if the project was successfully created before proceeding
826
+ if not final_destination.exists():
827
+ logging.error(
828
+ f"Final destination directory not found at {final_destination}"
829
+ )
795
830
  raise FileNotFoundError(
796
- f"Generated project directory not found at {output_dir}"
831
+ f"Final destination directory not found at {final_destination}"
832
+ )
833
+
834
+ # Render and merge Makefiles.
835
+ # If it's a local template, remote_template_path will be None,
836
+ # and only the base Makefile will be rendered.
837
+ render_and_merge_makefiles(
838
+ base_template_path=base_template_path,
839
+ final_destination=final_destination,
840
+ cookiecutter_config=cookiecutter_config,
841
+ remote_template_path=remote_template_path,
842
+ )
843
+
844
+ # Delete appropriate files based on ADK tag
845
+ if "adk" in tags:
846
+ files_to_delete = [final_destination / f for f in NON_ADK_FILES]
847
+ else:
848
+ files_to_delete = [final_destination / f for f in ADK_FILES]
849
+
850
+ for file_path in files_to_delete:
851
+ if file_path.exists():
852
+ file_path.unlink()
853
+ logging.debug(f"Deleted {file_path}")
854
+
855
+ # Clean up unused_* files and directories created by conditional templates
856
+ import glob
857
+
858
+ unused_patterns = [
859
+ final_destination / "unused_*",
860
+ final_destination / "**" / "unused_*",
861
+ ]
862
+
863
+ for pattern in unused_patterns:
864
+ for unused_path_str in glob.glob(str(pattern), recursive=True):
865
+ unused_path = pathlib.Path(unused_path_str)
866
+ if unused_path.exists():
867
+ if unused_path.is_dir():
868
+ shutil.rmtree(unused_path)
869
+ logging.debug(f"Deleted unused directory: {unused_path}")
870
+ else:
871
+ unused_path.unlink()
872
+ logging.debug(f"Deleted unused file: {unused_path}")
873
+
874
+ # Handle pyproject.toml and uv.lock files
875
+ if is_remote and remote_template_path:
876
+ # For remote templates, use their pyproject.toml and uv.lock if they exist
877
+ remote_pyproject = remote_template_path / "pyproject.toml"
878
+ remote_uv_lock = remote_template_path / "uv.lock"
879
+
880
+ if remote_pyproject.exists():
881
+ shutil.copy2(remote_pyproject, final_destination / "pyproject.toml")
882
+ logging.debug("Used pyproject.toml from remote template")
883
+
884
+ if remote_uv_lock.exists():
885
+ shutil.copy2(remote_uv_lock, final_destination / "uv.lock")
886
+ logging.debug("Used uv.lock from remote template")
887
+ elif deployment_target:
888
+ # For local templates, use the existing logic
889
+ lock_path = (
890
+ pathlib.Path(__file__).parent.parent.parent.parent
891
+ / "src"
892
+ / "resources"
893
+ / "locks"
894
+ / f"uv-{agent_name}-{deployment_target}.lock"
797
895
  )
896
+ logging.debug(f"Looking for lock file at: {lock_path}")
897
+ logging.debug(f"Lock file exists: {lock_path.exists()}")
898
+ if not lock_path.exists():
899
+ raise FileNotFoundError(f"Lock file not found: {lock_path}")
900
+ # Copy and rename to uv.lock in the project directory
901
+ shutil.copy2(lock_path, final_destination / "uv.lock")
902
+ logging.debug(
903
+ f"Copied lock file from {lock_path} to {final_destination}/uv.lock"
904
+ )
905
+
906
+ # Replace cookiecutter project name with actual project name in lock file
907
+ lock_file_path = final_destination / "uv.lock"
908
+ with open(lock_file_path, "r+", encoding="utf-8") as f:
909
+ content = f.read()
910
+ f.seek(0)
911
+ f.write(
912
+ content.replace("{{cookiecutter.project_name}}", project_name)
913
+ )
914
+ f.truncate()
915
+ logging.debug(f"Updated project name in lock file at {lock_file_path}")
798
916
 
799
917
  except Exception as e:
800
918
  logging.error(f"Failed to process template: {e!s}")
@@ -208,6 +208,7 @@ def deploy_agent_engine_app(
208
208
  requirements_file: str = ".requirements.txt",
209
209
  extra_packages: list[str] = ["./app"],
210
210
  env_vars: dict[str, str] = {},
211
+ service_account: str | None = None,
211
212
  ) -> agent_engines.AgentEngine:
212
213
  """Deploy the agent engine app to Vertex AI."""
213
214
 
@@ -247,6 +248,7 @@ def deploy_agent_engine_app(
247
248
  "description": "{{cookiecutter.agent_description}}",
248
249
  "extra_packages": extra_packages,
249
250
  "env_vars": env_vars,
251
+ "service_account": service_account,
250
252
  }
251
253
  logging.info(f"Agent config: {agent_config}")
252
254
  agent_config["requirements"] = requirements
@@ -310,6 +312,11 @@ if __name__ == "__main__":
310
312
  "--set-env-vars",
311
313
  help="Comma-separated list of environment variables in KEY=VALUE format",
312
314
  )
315
+ parser.add_argument(
316
+ "--service-account",
317
+ default=None,
318
+ help="Service account email to use for the agent engine",
319
+ )
313
320
  args = parser.parse_args()
314
321
 
315
322
  # Parse environment variables if provided
@@ -337,4 +344,5 @@ if __name__ == "__main__":
337
344
  requirements_file=args.requirements_file,
338
345
  extra_packages=args.extra_packages,
339
346
  env_vars=env_vars,
347
+ service_account=args.service_account,
340
348
  )
@@ -157,7 +157,7 @@ def stream_messages(
157
157
  set_tracing_properties(config)
158
158
  input_dict = input.model_dump()
159
159
 
160
- for data in agent.stream(input_dict, config=config, stream_mode="messages"):
160
+ for data in agent.stream(input_dict, config=config, stream_mode="messages"): # type: ignore[arg-type]
161
161
  yield dumps(data) + "\n"
162
162
 
163
163
 
@@ -193,7 +193,7 @@ resource "google_cloud_run_v2_service" "app" {
193
193
  {%- endif %}
194
194
  }
195
195
 
196
- service_account = google_service_account.cloud_run_app_sa.email
196
+ service_account = google_service_account.app_sa.email
197
197
  max_instance_request_concurrency = 40
198
198
 
199
199
  scaling {
@@ -216,7 +216,7 @@ resource "google_cloud_run_v2_service" "app_staging" {
216
216
  {%- endif %}
217
217
  }
218
218
 
219
- service_account = google_service_account.cloud_run_app_sa["staging"].email
219
+ service_account = google_service_account.app_sa["staging"].email
220
220
  max_instance_request_concurrency = 40
221
221
 
222
222
  scaling {
@@ -322,7 +322,7 @@ resource "google_cloud_run_v2_service" "app_prod" {
322
322
  {%- endif %}
323
323
  }
324
324
 
325
- service_account = google_service_account.cloud_run_app_sa["prod"].email
325
+ service_account = google_service_account.app_sa["prod"].email
326
326
  max_instance_request_concurrency = 40
327
327
 
328
328
  scaling {