agent-starter-pack 0.9.0__py3-none-any.whl → 0.9.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-starter-pack
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: CLI to bootstrap production-ready Google Cloud GenAI agent projects from templates.
5
5
  Author-email: Google LLC <agent-starter-pack@google.com>
6
6
  License: Apache-2.0
@@ -16,11 +16,9 @@ Provides-Extra: jupyter
16
16
  Requires-Dist: ipykernel>=6.29.5; extra == 'jupyter'
17
17
  Requires-Dist: jupyter; extra == 'jupyter'
18
18
  Provides-Extra: lint
19
- Requires-Dist: codespell~=2.2.0; extra == 'lint'
20
- Requires-Dist: mypy~=1.15.0; extra == 'lint'
21
- Requires-Dist: ruff>=0.4.6; extra == 'lint'
22
- Requires-Dist: types-pyyaml~=6.0.12.20240917; extra == 'lint'
23
- Requires-Dist: types-requests~=2.32.0.20240914; extra == 'lint'
19
+ Requires-Dist: codespell; extra == 'lint'
20
+ Requires-Dist: mypy; extra == 'lint'
21
+ Requires-Dist: ruff; extra == 'lint'
24
22
  Description-Content-Type: text/markdown
25
23
 
26
24
  # 🚀 Agent Starter Pack
@@ -74,16 +74,16 @@ src/base_template/deployment/terraform/dev/vars/env.tfvars,sha256=LoQMjh1AAMR-MG
74
74
  src/base_template/deployment/terraform/vars/env.tfvars,sha256=Nze8q1x2Aj6ZUeWC2hDeZWqNUkLp13DgzA_LFmmOzCo,1216
75
75
  src/base_template/tests/unit/test_dummy.py,sha256=2exfCH8qhkZrLWvK04ZxNTO9MV3fdTbZkJN3uK6zvok,850
76
76
  src/cli/main.py,sha256=fyJKjU1gvRQmuqS-J6sExvqXo0-z7n6Bce8PSbDPa6E,1769
77
- src/cli/commands/create.py,sha256=yIHngbbeUBvLKWg6IkH87yFwf-IJqdzbebkdqpwEFRQ,31818
78
- src/cli/commands/list.py,sha256=50sxEcYY5T4Q34Uj7J5h8A9Tq5Zw8GQhkqunBdzzD0U,5288
77
+ src/cli/commands/create.py,sha256=rdN_mWiIxwddsbIYVkrQCApK2x9ABQeSXZZoGtcE074,34764
78
+ src/cli/commands/list.py,sha256=sMMGJiW_IjRUe-Md1bI_8B9wruYJ87JLSEoJfHaypbg,5038
79
79
  src/cli/commands/setup_cicd.py,sha256=1ZvgTD-Z2bZk6pWuinz4IP2G1Eb8H1MibRLPS-mZ6Ng,31721
80
80
  src/cli/utils/__init__.py,sha256=_cTmsXGPqOtK0q8UW5164QTltbJRJFR_Efxq_BRL1-o,1311
81
81
  src/cli/utils/cicd.py,sha256=VSkJTL7OBnXQ6Zbb2gzgw5gLWpwUjfr9XthqTpJ2Oj8,26197
82
82
  src/cli/utils/datastores.py,sha256=gv1V6eDcOEKx4MRNG5C3Y-VfixYq1AzQuaYMLp8QRNo,1058
83
83
  src/cli/utils/gcp.py,sha256=cnuCyN144eiyYc9aJNEK9JnyWN66rdevugoMdDYC1UU,4032
84
84
  src/cli/utils/logging.py,sha256=0lHe4EPi1A8sOx9xkA7gS4UNl0GsIyp2ahydkkuCzLY,1570
85
- src/cli/utils/remote_template.py,sha256=LA2gAo1i_PCMMB6gqWPOH-WWR8MFyr9CrK9_-f1jVIY,7895
86
- src/cli/utils/template.py,sha256=GpUWMQ7hfO5nkACRQ3_aFbn5yT2MjO58nr9Hs0Fp8kM,33700
85
+ src/cli/utils/remote_template.py,sha256=RYphQS1IZfzzgtgWJOLdgie0S8HtEWjr2wikyI2doTc,10454
86
+ src/cli/utils/template.py,sha256=ciElaBHEMBLYIzZJLQwNe_rOc0C_HqJfEGJ6z9p1ep4,34320
87
87
  src/cli/utils/version.py,sha256=F4udQmzniPStqWZFIgnv3Qg3l9non4mfy2An-Oveqmc,2916
88
88
  src/data_ingestion/README.md,sha256=LNxSQoJW9JozK-TbyGQLj5L_MGWNwrfLk6V6RmQ2oBQ,4032
89
89
  src/data_ingestion/pyproject.toml,sha256=-1Mf2QB8K70ICQV5UPZDpf-fN3UwEQLVzQyxfakCSTY,445
@@ -179,7 +179,7 @@ src/frontends/streamlit/frontend/utils/multimodal_utils.py,sha256=v6YbCkz_YcnEo-
179
179
  src/frontends/streamlit/frontend/utils/stream_handler.py,sha256=-XVRfLH6ck1KP6NS4vrhcvPyZe6z3NLHliEg17s9jjo,12551
180
180
  src/frontends/streamlit/frontend/utils/title_summary.py,sha256=B0cadS_KPW-tsbABauI4J681aqjEtuKFDa25e9R1WKc,3030
181
181
  src/resources/containers/data_processing/Dockerfile,sha256=VoB9d5yZiiWnqRfWrIq0gGNMzZg-eVy733OgP72ZgO0,950
182
- src/resources/containers/e2e-tests/Dockerfile,sha256=Q_aTyX_iaFY8j06XZkpMuggJnNO5daiLmmrvqaZHMxw,1611
182
+ src/resources/containers/e2e-tests/Dockerfile,sha256=yA3HxeX0HNpl8B4oEQUICCVZDCXdRn2Igmii4jt-r7k,1895
183
183
  src/resources/docs/adk-cheatsheet.md,sha256=KTCdM7hOvsv1-GgwEBMMU6kLk3WHQ69RAs6VWltNaDA,58722
184
184
  src/resources/idx/idx-template.json,sha256=07OQZCPp45Iqor2O7Tm1e1Gmsdd-AmmUofSvA7y-oRs,793
185
185
  src/resources/idx/idx-template.nix,sha256=sesHGev_PYtVDg0J5tHkg0OO7IR1Bz2iAtl_if3Ar3M,892
@@ -201,8 +201,8 @@ src/resources/setup_cicd/providers.tf,sha256=Km4z6IJt7x7PLaa0kyZbBrO2m3lpuIJZFD5
201
201
  src/utils/generate_locks.py,sha256=6V1B8V2BEuevWnXUsxZVTrLjXwFRII8UfsIGrQqZxVs,4320
202
202
  src/utils/lock_utils.py,sha256=IFOMUWtb-ypm2Y8w8J5y2oI_-MaPuwPF_JOAAlnNudA,2275
203
203
  src/utils/watch_and_rebuild.py,sha256=vP4yIiA7E_lj5sfQdJUl8TXas6V7msDg8XWUutAC05Q,6679
204
- agent_starter_pack-0.9.0.dist-info/METADATA,sha256=tm8hhXSmxExEPbCj30Q1GlG9U0W5xiLP84JTavvezJI,11286
205
- agent_starter_pack-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
206
- agent_starter_pack-0.9.0.dist-info/entry_points.txt,sha256=U7uCxR7YulIhZ0L8R8Hui0Bsy6J7oyESBeDYJYMrQjA,56
207
- agent_starter_pack-0.9.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
208
- agent_starter_pack-0.9.0.dist-info/RECORD,,
204
+ agent_starter_pack-0.9.2.dist-info/METADATA,sha256=bC09qQ-oHxiJGHYA-3aa1AaSE42t6IIa_Y2BE3Vd33o,11138
205
+ agent_starter_pack-0.9.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
206
+ agent_starter_pack-0.9.2.dist-info/entry_points.txt,sha256=U7uCxR7YulIhZ0L8R8Hui0Bsy6J7oyESBeDYJYMrQjA,56
207
+ agent_starter_pack-0.9.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
208
+ agent_starter_pack-0.9.2.dist-info/RECORD,,
@@ -15,14 +15,16 @@
15
15
  import logging
16
16
  import os
17
17
  import pathlib
18
+ import shutil
18
19
  import subprocess
20
+ import tempfile
19
21
 
20
22
  import click
21
23
  from click.core import ParameterSource
22
24
  from rich.console import Console
23
25
  from rich.prompt import IntPrompt, Prompt
24
26
 
25
- from ..utils.datastores import DATASTORE_TYPES
27
+ from ..utils.datastores import DATASTORE_TYPES, DATASTORES
26
28
  from ..utils.gcp import verify_credentials, verify_vertex_connection
27
29
  from ..utils.logging import handle_cli_error
28
30
  from ..utils.remote_template import (
@@ -34,6 +36,7 @@ from ..utils.remote_template import (
34
36
  )
35
37
  from ..utils.template import (
36
38
  get_available_agents,
39
+ get_deployment_targets,
37
40
  get_template_path,
38
41
  load_template_config,
39
42
  process_template,
@@ -128,7 +131,7 @@ def normalize_project_name(project_name: str) -> str:
128
131
  @click.option(
129
132
  "--skip-checks",
130
133
  is_flag=True,
131
- help="Skip verification checks for uv, GCP and Vertex AI",
134
+ help="Skip verification checks for GCP and Vertex AI",
132
135
  default=False,
133
136
  )
134
137
  @handle_cli_error
@@ -155,7 +158,7 @@ def create(
155
158
  style="bold blue",
156
159
  )
157
160
  console.print(
158
- "Powered by ([link=https://goo.gle/agent-starter-pack]Google's Agent Starter Pack [/link])\n",
161
+ "Powered by [link=https://goo.gle/agent-starter-pack]Google Cloud - Agent Starter Pack [/link]\n",
159
162
  )
160
163
  console.print(
161
164
  "This tool will help you create an end-to-end production-ready AI agent in Google Cloud!\n"
@@ -201,17 +204,28 @@ def create(
201
204
  # Agent selection - handle remote templates
202
205
  selected_agent = None
203
206
  template_source_path = None
207
+ temp_dir_to_clean = None
204
208
 
205
209
  if agent:
206
210
  if agent.startswith("local@"):
207
211
  path_str = agent.split("@", 1)[1]
208
- template_source_path = pathlib.Path(path_str).resolve()
209
- if not template_source_path.is_dir():
212
+ local_path = pathlib.Path(path_str).resolve()
213
+ if not local_path.is_dir():
210
214
  raise click.ClickException(
211
- f"Local path not found or not a directory: {template_source_path}"
215
+ f"Local path not found or not a directory: {local_path}"
212
216
  )
217
+
218
+ # Create a temporary directory and copy the local template to it
219
+ temp_dir = tempfile.mkdtemp(prefix="asp_local_template_")
220
+ temp_dir_to_clean = temp_dir
221
+ template_source_path = pathlib.Path(temp_dir) / local_path.name
222
+ shutil.copytree(local_path, template_source_path)
223
+
213
224
  selected_agent = f"local_{template_source_path.name}"
214
- console.print(f"Using local template: {template_source_path}")
225
+ console.print(f"Using local template: {local_path}")
226
+ logging.debug(
227
+ f"Copied local template to temporary dir: {template_source_path}"
228
+ )
215
229
  else:
216
230
  # Check if it's a remote template specification
217
231
  remote_spec = parse_agent_spec(agent)
@@ -223,7 +237,10 @@ def create(
223
237
  )
224
238
  else:
225
239
  console.print(f"Fetching remote template: {agent}")
226
- template_source_path = fetch_remote_template(remote_spec)
240
+ template_source_path, temp_dir_path = fetch_remote_template(
241
+ remote_spec
242
+ )
243
+ temp_dir_to_clean = str(temp_dir_path)
227
244
  selected_agent = f"remote_{hash(agent)}" # Generate unique name for remote template
228
245
  else:
229
246
  # Handle local agent selection
@@ -244,13 +261,17 @@ def create(
244
261
  f"Invalid agent name or number: {agent}"
245
262
  ) from err
246
263
 
247
- final_agent = (
248
- selected_agent
249
- if selected_agent
250
- else display_agent_selection(deployment_target)
251
- )
264
+ # Agent selection
265
+ final_agent = selected_agent
266
+ if not final_agent:
267
+ if auto_approve:
268
+ raise click.ClickException(
269
+ "Error: --agent is required when running with --auto-approve."
270
+ )
271
+ final_agent = display_agent_selection(deployment_target)
272
+
252
273
  if debug:
253
- logging.debug(f"Selected agent: {agent}")
274
+ logging.debug(f"Selected agent: {final_agent}")
254
275
 
255
276
  # Load template configuration based on whether it's remote or local
256
277
  if template_source_path:
@@ -302,14 +323,19 @@ def create(
302
323
  config = load_template_config(template_path)
303
324
  # Data ingestion and datastore selection
304
325
  if include_data_ingestion or datastore:
305
- # If datastore is specified but include_data_ingestion is not, set it to True
306
326
  include_data_ingestion = True
307
-
308
- # If include_data_ingestion is True but no datastore is specified, prompt for it
309
327
  if not datastore:
310
- # Pass a flag to indicate this is from explicit CLI flag
311
- datastore = prompt_datastore_selection(final_agent, from_cli_flag=True)
312
-
328
+ if auto_approve:
329
+ # Default to the first available datastore in non-interactive mode
330
+ datastore = next(iter(DATASTORES.keys()))
331
+ console.print(
332
+ f"Info: --datastore not specified. Defaulting to '{datastore}' in auto-approve mode.",
333
+ style="yellow",
334
+ )
335
+ else:
336
+ datastore = prompt_datastore_selection(
337
+ final_agent, from_cli_flag=True
338
+ )
313
339
  if debug:
314
340
  logging.debug(f"Data ingestion enabled: {include_data_ingestion}")
315
341
  logging.debug(f"Selected datastore type: {datastore}")
@@ -317,8 +343,15 @@ def create(
317
343
  # Check if the agent requires data ingestion
318
344
  if config and config.get("settings", {}).get("requires_data_ingestion"):
319
345
  include_data_ingestion = True
320
- datastore = prompt_datastore_selection(final_agent)
321
-
346
+ if not datastore:
347
+ if auto_approve:
348
+ datastore = next(iter(DATASTORES.keys()))
349
+ console.print(
350
+ f"Info: --datastore not specified. Defaulting to '{datastore}' in auto-approve mode.",
351
+ style="yellow",
352
+ )
353
+ else:
354
+ datastore = prompt_datastore_selection(final_agent)
322
355
  if debug:
323
356
  logging.debug(
324
357
  f"Data ingestion required by agent: {include_data_ingestion}"
@@ -334,13 +367,25 @@ def create(
334
367
  deployment_agent_name = get_base_template_name(config)
335
368
  remote_config = config
336
369
 
337
- final_deployment = (
338
- deployment_target
339
- if deployment_target
340
- else prompt_deployment_target(
370
+ final_deployment = deployment_target
371
+ if not final_deployment:
372
+ available_targets = get_deployment_targets(
341
373
  deployment_agent_name, remote_config=remote_config
342
374
  )
343
- )
375
+ if auto_approve:
376
+ if not available_targets:
377
+ raise click.ClickException(
378
+ f"Error: No deployment targets available for agent '{deployment_agent_name}'."
379
+ )
380
+ final_deployment = available_targets[0]
381
+ console.print(
382
+ f"Info: --deployment-target not specified. Defaulting to '{final_deployment}' in auto-approve mode.",
383
+ style="yellow",
384
+ )
385
+ else:
386
+ final_deployment = prompt_deployment_target(
387
+ deployment_agent_name, remote_config=remote_config
388
+ )
344
389
  if debug:
345
390
  logging.debug(f"Selected deployment target: {final_deployment}")
346
391
 
@@ -359,8 +404,19 @@ def create(
359
404
  )
360
405
  return
361
406
 
362
- if final_deployment in ("cloud_run") and not session_type:
363
- final_session_type = prompt_session_type_selection()
407
+ if (
408
+ final_deployment is not None
409
+ and final_deployment in ("cloud_run")
410
+ and not session_type
411
+ ):
412
+ if auto_approve:
413
+ final_session_type = "in_memory"
414
+ console.print(
415
+ "Info: --session-type not specified. Defaulting to 'in_memory' in auto-approve mode.",
416
+ style="yellow",
417
+ )
418
+ else:
419
+ final_session_type = prompt_session_type_selection()
364
420
  else:
365
421
  # Agents that don't require session management always use in-memory sessions
366
422
  final_session_type = "in_memory"
@@ -441,10 +497,22 @@ def create(
441
497
  remote_template_path=template_source_path,
442
498
  remote_config=config if template_source_path else None,
443
499
  )
444
- finally:
500
+
445
501
  # Replace region in all files if a different region was specified
446
502
  if region != "us-central1":
447
503
  replace_region_in_files(project_path, region, debug=debug)
504
+ finally:
505
+ # Clean up the temporary directory if one was created
506
+ if temp_dir_to_clean:
507
+ try:
508
+ shutil.rmtree(temp_dir_to_clean)
509
+ logging.debug(
510
+ f"Successfully cleaned up temporary directory: {temp_dir_to_clean}"
511
+ )
512
+ except OSError as e:
513
+ logging.warning(
514
+ f"Failed to clean up temporary directory {temp_dir_to_clean}: {e}"
515
+ )
448
516
 
449
517
  project_path = destination_dir / project_name
450
518
  cd_path = project_path if output_dir else project_name
@@ -474,14 +542,12 @@ def create(
474
542
  console.print("\n🚀 To get started, run the following command:")
475
543
 
476
544
  # Check if the agent has a 'dev' command in its settings
477
- if config["settings"].get("commands", {}).get("extra", {}).get("dev"):
478
- console.print(
479
- f" [bold bright_green]cd {cd_path} && make install && make dev[/]"
480
- )
481
- else:
482
- console.print(
483
- f" [bold bright_green]cd {cd_path} && make install && make playground[/]"
484
- )
545
+ interactive_command = config.get("settings", {}).get(
546
+ "interactive_command", "playground"
547
+ )
548
+ console.print(
549
+ f" [bold bright_green]cd {cd_path} && make install && make {interactive_command}[/]"
550
+ )
485
551
  except Exception:
486
552
  if debug:
487
553
  logging.exception(
src/cli/commands/list.py CHANGED
@@ -82,15 +82,9 @@ def list_remote_agents(remote_source: str, scan_from_root: bool = False) -> None
82
82
  # specific template directory within the repo.
83
83
  template_dir_path = fetch_remote_template(spec)
84
84
 
85
- if scan_from_root:
86
- # For ADK, we want to scan the entire repo. We need to find the
87
- # root of the repo and scan from there.
88
- scan_path = template_dir_path
89
- while not (scan_path / ".git").exists() and scan_path.parent != scan_path:
90
- scan_path = scan_path.parent
91
- else:
92
- # For other git repos, respect the path given in the URL.
93
- scan_path = template_dir_path
85
+ # fetch_remote_template always returns a tuple of (repo_path, template_path)
86
+ repo_path, template_path = template_dir_path
87
+ scan_path = repo_path if scan_from_root else template_path
94
88
 
95
89
  display_agents_from_path(scan_path, remote_source)
96
90
 
@@ -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)
@@ -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) 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) 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") 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"]
@@ -622,6 +623,7 @@ def process_template(
622
623
  ".pytest_cache/*",
623
624
  ".venv/*",
624
625
  "*templates.py", # Don't render templates files
626
+ "Makefile", # Don't render Makefile - handled by render_and_merge_makefiles
625
627
  # Don't render agent.py unless it's agentic_rag
626
628
  "app/agent.py" if agent_name != "agentic_rag" else "",
627
629
  ],
@@ -658,6 +660,16 @@ def process_template(
658
660
  shutil.copytree(output_dir, final_destination, dirs_exist_ok=True)
659
661
  logging.debug(f"Project successfully created at {final_destination}")
660
662
 
663
+ # Render and merge Makefiles.
664
+ # If it's a local template, remote_template_path will be None,
665
+ # and only the base Makefile will be rendered.
666
+ render_and_merge_makefiles(
667
+ base_template_path=base_template_path,
668
+ final_destination=final_destination,
669
+ cookiecutter_config=cookiecutter_config,
670
+ remote_template_path=remote_template_path,
671
+ )
672
+
661
673
  # Delete appropriate files based on ADK tag
662
674
  if "adk" in tags:
663
675
  files_to_delete = [final_destination / f for f in NON_ADK_FILES]
@@ -1,19 +1,41 @@
1
- FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
2
- RUN apt-get update && apt-get install -y wget sudo apt-transport-https ca-certificates gnupg curl software-properties-common && \
3
- # Setup GitHub CLI
4
- sudo mkdir -p -m 755 /etc/apt/keyrings && \
5
- wget -q -O- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null && \
6
- sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg && \
7
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \
8
- # Setup Google Cloud SDK
9
- echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \
10
- curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - && \
11
- # Setup Terraform
12
- wget -q -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null && \
13
- echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list > /dev/null && \
14
- # Install all packages at once
15
- apt-get update && \
16
- apt-get install -y gh google-cloud-cli terraform && \
17
- # Clean up
1
+ # Stage 1: Builder - Prepare APT repositories
2
+ FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim AS builder
3
+
4
+ # Install tools needed to add repositories
5
+ RUN apt-get update && \
6
+ apt-get install -y --no-install-recommends \
7
+ ca-certificates \
8
+ curl \
9
+ gnupg && \
10
+ # GitHub CLI
11
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | gpg --dearmor -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
12
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list && \
13
+ # Google Cloud SDK
14
+ curl -sS https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg && \
15
+ echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list && \
16
+ # Terraform
17
+ curl -fsSL https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg && \
18
+ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com bookworm main" > /etc/apt/sources.list.d/hashicorp.list && \
19
+ # Clean up builder stage
18
20
  apt-get clean && \
19
21
  rm -rf /var/lib/apt/lists/*
22
+
23
+ # Stage 2: Final Image
24
+ FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
25
+
26
+ # Copy repository configurations from the builder stage
27
+ COPY --from=builder /etc/apt/sources.list.d/ /etc/apt/sources.list.d/
28
+ COPY --from=builder /usr/share/keyrings/ /usr/share/keyrings/
29
+
30
+ # Install the final packages
31
+ RUN apt-get update && \
32
+ apt-get install -y --no-install-recommends \
33
+ gh \
34
+ google-cloud-cli \
35
+ terraform \
36
+ make \
37
+ nodejs \
38
+ npm && \
39
+ # Clean up apt cache
40
+ apt-get clean && \
41
+ rm -rf /var/lib/apt/lists/*