agent-starter-pack 0.8.0__py3-none-any.whl → 0.9.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 (30) hide show
  1. {agent_starter_pack-0.8.0.dist-info → agent_starter_pack-0.9.0.dist-info}/METADATA +3 -2
  2. {agent_starter_pack-0.8.0.dist-info → agent_starter_pack-0.9.0.dist-info}/RECORD +30 -28
  3. agents/adk_base/{template/.templateconfig.yaml → .template/templateconfig.yaml} +1 -1
  4. agents/adk_gemini_fullstack/{template/.templateconfig.yaml → .template/templateconfig.yaml} +2 -2
  5. agents/agentic_rag/{template/.templateconfig.yaml → .template/templateconfig.yaml} +1 -1
  6. src/base_template/Makefile +5 -1
  7. src/base_template/pyproject.toml +2 -2
  8. src/cli/commands/create.py +155 -43
  9. src/cli/commands/list.py +158 -0
  10. src/cli/main.py +2 -0
  11. src/cli/utils/remote_template.py +254 -0
  12. src/cli/utils/template.py +100 -25
  13. src/resources/locks/uv-adk_base-agent_engine.lock +151 -119
  14. src/resources/locks/uv-adk_base-cloud_run.lock +181 -146
  15. src/resources/locks/uv-adk_gemini_fullstack-agent_engine.lock +151 -119
  16. src/resources/locks/uv-adk_gemini_fullstack-cloud_run.lock +181 -146
  17. src/resources/locks/uv-agentic_rag-agent_engine.lock +154 -122
  18. src/resources/locks/uv-agentic_rag-cloud_run.lock +184 -149
  19. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +130 -130
  20. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +160 -160
  21. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +109 -109
  22. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +139 -139
  23. src/resources/locks/uv-live_api-cloud_run.lock +136 -136
  24. src/utils/lock_utils.py +1 -1
  25. {agent_starter_pack-0.8.0.dist-info → agent_starter_pack-0.9.0.dist-info}/WHEEL +0 -0
  26. {agent_starter_pack-0.8.0.dist-info → agent_starter_pack-0.9.0.dist-info}/entry_points.txt +0 -0
  27. {agent_starter_pack-0.8.0.dist-info → agent_starter_pack-0.9.0.dist-info}/licenses/LICENSE +0 -0
  28. /agents/crewai_coding_crew/{template/.templateconfig.yaml → .template/templateconfig.yaml} +0 -0
  29. /agents/langgraph_base_react/{template/.templateconfig.yaml → .template/templateconfig.yaml} +0 -0
  30. /agents/live_api/{template/.templateconfig.yaml → .template/templateconfig.yaml} +0 -0
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.list import list_agents
21
22
  from .commands.setup_cicd import setup_cicd
22
23
  from .utils import display_update_message
23
24
 
@@ -53,6 +54,7 @@ def cli() -> None:
53
54
  # Register commands
54
55
  cli.add_command(create)
55
56
  cli.add_command(setup_cicd)
57
+ cli.add_command(list_agents, name="list")
56
58
 
57
59
 
58
60
  if __name__ == "__main__":
@@ -0,0 +1,254 @@
1
+ # Copyright 2025 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import logging
16
+ import os
17
+ import pathlib
18
+ import re
19
+ import shutil
20
+ import subprocess
21
+ import tempfile
22
+ from dataclasses import dataclass
23
+ from typing import Any
24
+
25
+ import yaml
26
+
27
+
28
+ @dataclass
29
+ class RemoteTemplateSpec:
30
+ """Parsed remote template specification."""
31
+
32
+ repo_url: str
33
+ template_path: str
34
+ git_ref: str
35
+ is_adk_samples: bool = False
36
+
37
+
38
+ def parse_agent_spec(agent_spec: str) -> RemoteTemplateSpec | None:
39
+ """Parse agent specification to determine if it's a remote template.
40
+
41
+ Args:
42
+ agent_spec: Agent specification string
43
+
44
+ Returns:
45
+ RemoteTemplateSpec if remote template, None if local template
46
+ """
47
+ # Check for local@ prefix
48
+ if agent_spec.startswith("local@"):
49
+ return None
50
+
51
+ # Check for adk@ shortcut
52
+ if agent_spec.startswith("adk@"):
53
+ sample_name = agent_spec[4:] # Remove "adk@" prefix
54
+ return RemoteTemplateSpec(
55
+ repo_url="https://github.com/google/adk-samples",
56
+ template_path=f"python/agents/{sample_name}",
57
+ git_ref="main",
58
+ is_adk_samples=True,
59
+ )
60
+
61
+ # GitHub /tree/ URL pattern
62
+ tree_pattern = r"^(https?://[^/]+/[^/]+/[^/]+)/tree/([^/]+)/(.*)$"
63
+ match = re.match(tree_pattern, agent_spec)
64
+ if match:
65
+ repo_url = match.group(1)
66
+ git_ref = match.group(2)
67
+ template_path = match.group(3)
68
+ return RemoteTemplateSpec(
69
+ repo_url=repo_url,
70
+ template_path=template_path.strip("/"),
71
+ git_ref=git_ref,
72
+ )
73
+
74
+ # General remote pattern: <repo_url>[/<path>][@<ref>]
75
+ # Handles github.com, gitlab.com, etc.
76
+ remote_pattern = r"^(https?://[^/]+/[^/]+/[^/]+)(?:/(.*?))?(?:@([^/]+))?/?$"
77
+ match = re.match(remote_pattern, agent_spec)
78
+ if match:
79
+ repo_url = match.group(1)
80
+ template_path_with_ref = match.group(2) or ""
81
+ git_ref_from_url = match.group(3)
82
+
83
+ # Separate path and ref if ref is part of the path
84
+ template_path = template_path_with_ref
85
+ git_ref = git_ref_from_url or "main"
86
+
87
+ if "@" in template_path:
88
+ path_parts = template_path.split("@")
89
+ template_path = path_parts[0]
90
+ git_ref = path_parts[1]
91
+
92
+ return RemoteTemplateSpec(
93
+ repo_url=repo_url,
94
+ template_path=template_path.strip("/"),
95
+ git_ref=git_ref,
96
+ )
97
+
98
+ # GitHub shorthand: <org>/<repo>[/<path>][@<ref>]
99
+ github_shorthand_pattern = r"^([^/]+)/([^/]+)(?:/(.*?))?(?:@([^/]+))?/?$"
100
+ match = re.match(github_shorthand_pattern, agent_spec)
101
+ if match and "/" in agent_spec: # Ensure it has at least one slash
102
+ org = match.group(1)
103
+ repo = match.group(2)
104
+ template_path = match.group(3) or ""
105
+ git_ref = match.group(4) or "main"
106
+ return RemoteTemplateSpec(
107
+ repo_url=f"https://github.com/{org}/{repo}",
108
+ template_path=template_path,
109
+ git_ref=git_ref,
110
+ )
111
+
112
+ return None
113
+
114
+
115
+ def fetch_remote_template(spec: RemoteTemplateSpec) -> pathlib.Path:
116
+ """Fetch remote template and return path to template directory.
117
+
118
+ Uses Git to clone the remote repository.
119
+
120
+ Args:
121
+ spec: Remote template specification
122
+
123
+ Returns:
124
+ Path to the fetched template directory
125
+ """
126
+ temp_dir = tempfile.mkdtemp(prefix="asp_remote_template_")
127
+ temp_path = pathlib.Path(temp_dir)
128
+ repo_path = temp_path / "repo"
129
+
130
+ # Attempt Git Clone
131
+ try:
132
+ clone_url = spec.repo_url
133
+ clone_cmd = [
134
+ "git",
135
+ "clone",
136
+ "--depth",
137
+ "1",
138
+ "--branch",
139
+ spec.git_ref,
140
+ clone_url,
141
+ str(repo_path),
142
+ ]
143
+ logging.debug(
144
+ f"Attempting to clone remote template with Git: {' '.join(clone_cmd)}"
145
+ )
146
+ # GIT_TERMINAL_PROMPT=0 prevents git from prompting for credentials
147
+ subprocess.run(
148
+ clone_cmd,
149
+ capture_output=True,
150
+ text=True,
151
+ check=True,
152
+ encoding="utf-8",
153
+ env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
154
+ )
155
+ logging.debug("Git clone successful.")
156
+ except subprocess.CalledProcessError as e:
157
+ shutil.rmtree(temp_path, ignore_errors=True)
158
+ raise RuntimeError(f"Git clone failed: {e.stderr.strip()}") from e
159
+
160
+ # Process the successfully fetched template
161
+ try:
162
+ if spec.template_path:
163
+ template_dir = repo_path / spec.template_path
164
+ else:
165
+ template_dir = repo_path
166
+
167
+ if not template_dir.exists():
168
+ raise FileNotFoundError(
169
+ f"Template path not found in the repository: {spec.template_path}"
170
+ )
171
+
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
184
+ except Exception as e:
185
+ # Clean up on error
186
+ shutil.rmtree(temp_path, ignore_errors=True)
187
+ raise RuntimeError(
188
+ f"An unexpected error occurred after fetching remote template: {e}"
189
+ ) from e
190
+
191
+
192
+ def load_remote_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
193
+ """Load template configuration from remote template.
194
+
195
+ Args:
196
+ template_dir: Path to template directory
197
+
198
+ Returns:
199
+ Template configuration dictionary
200
+ """
201
+ config_path = template_dir / ".template" / "templateconfig.yaml"
202
+
203
+ if not config_path.exists():
204
+ return {}
205
+
206
+ try:
207
+ with open(config_path) as f:
208
+ config = yaml.safe_load(f)
209
+ return config if config else {}
210
+ except Exception as e:
211
+ logging.error(f"Error loading remote template config: {e}")
212
+ return {}
213
+
214
+
215
+ def get_base_template_name(config: dict[str, Any]) -> str:
216
+ """Get base template name from remote template config.
217
+
218
+ Args:
219
+ config: Template configuration dictionary
220
+
221
+ Returns:
222
+ Base template name (defaults to "adk_base")
223
+ """
224
+ return config.get("base_template", "adk_base")
225
+
226
+
227
+ def merge_template_configs(
228
+ base_config: dict[str, Any], remote_config: dict[str, Any]
229
+ ) -> dict[str, Any]:
230
+ """Merge base template config with remote template config using a deep merge.
231
+
232
+ Args:
233
+ base_config: Base template configuration
234
+ remote_config: Remote template configuration
235
+
236
+ Returns:
237
+ Merged configuration with remote overriding base
238
+ """
239
+ import copy
240
+
241
+ def deep_merge(d1: dict[str, Any], d2: dict[str, Any]) -> dict[str, Any]:
242
+ """Recursively merges d2 into d1."""
243
+ for k, v in d2.items():
244
+ if k in d1 and isinstance(d1[k], dict) and isinstance(v, dict):
245
+ d1[k] = deep_merge(d1[k], v)
246
+ else:
247
+ d1[k] = v
248
+ return d1
249
+
250
+ # Start with a deep copy of the base to avoid modifying it
251
+ merged_config = copy.deepcopy(base_config)
252
+
253
+ # Perform the deep merge
254
+ return deep_merge(merged_config, remote_config)
src/cli/utils/template.py CHANGED
@@ -29,6 +29,9 @@ from rich.prompt import IntPrompt, Prompt
29
29
  from src.cli.utils.version import get_current_version
30
30
 
31
31
  from .datastores import DATASTORES
32
+ from .remote_template import (
33
+ get_base_template_name,
34
+ )
32
35
 
33
36
  ADK_FILES = ["app/__init__.py"]
34
37
  NON_ADK_FILES: list[str] = []
@@ -69,7 +72,7 @@ class TemplateConfig:
69
72
 
70
73
 
71
74
  OVERWRITE_FOLDERS = ["app", "frontend", "tests", "notebooks"]
72
- TEMPLATE_CONFIG_FILE = ".templateconfig.yaml"
75
+ TEMPLATE_CONFIG_FILE = "templateconfig.yaml"
73
76
  DEPLOYMENT_FOLDERS = ["cloud_run", "agent_engine"]
74
77
  DEFAULT_FRONTEND = "streamlit"
75
78
 
@@ -94,7 +97,7 @@ def get_available_agents(deployment_target: str | None = None) -> dict:
94
97
 
95
98
  for agent_dir in agents_dir.iterdir():
96
99
  if agent_dir.is_dir() and not agent_dir.name.startswith("__"):
97
- template_config_path = agent_dir / "template" / ".templateconfig.yaml"
100
+ template_config_path = agent_dir / ".template" / "templateconfig.yaml"
98
101
  if template_config_path.exists():
99
102
  try:
100
103
  with open(template_config_path) as f:
@@ -154,15 +157,20 @@ def load_template_config(template_dir: pathlib.Path) -> dict[str, Any]:
154
157
  return {}
155
158
 
156
159
 
157
- def get_deployment_targets(agent_name: str) -> list:
160
+ def get_deployment_targets(
161
+ agent_name: str, remote_config: dict[str, Any] | None = None
162
+ ) -> list:
158
163
  """Get available deployment targets for the selected agent."""
159
- template_path = (
160
- pathlib.Path(__file__).parent.parent.parent.parent
161
- / "agents"
162
- / agent_name
163
- / "template"
164
- )
165
- config = load_template_config(template_path)
164
+ if remote_config:
165
+ config = remote_config
166
+ else:
167
+ template_path = (
168
+ pathlib.Path(__file__).parent.parent.parent.parent
169
+ / "agents"
170
+ / agent_name
171
+ / ".template"
172
+ )
173
+ config = load_template_config(template_path)
166
174
 
167
175
  if not config:
168
176
  return []
@@ -171,9 +179,11 @@ def get_deployment_targets(agent_name: str) -> list:
171
179
  return targets if isinstance(targets, list) else [targets]
172
180
 
173
181
 
174
- def prompt_deployment_target(agent_name: str) -> str:
182
+ def prompt_deployment_target(
183
+ agent_name: str, remote_config: dict[str, Any] | None = None
184
+ ) -> str:
175
185
  """Ask user to select a deployment target for the agent."""
176
- targets = get_deployment_targets(agent_name)
186
+ targets = get_deployment_targets(agent_name, remote_config=remote_config)
177
187
 
178
188
  # Define deployment target friendly names and descriptions
179
189
  TARGET_INFO = {
@@ -272,7 +282,7 @@ def prompt_datastore_selection(
272
282
  pathlib.Path(__file__).parent.parent.parent.parent
273
283
  / "agents"
274
284
  / agent_name
275
- / "template"
285
+ / ".template"
276
286
  )
277
287
  config = load_template_config(template_path)
278
288
 
@@ -349,7 +359,7 @@ def prompt_datastore_selection(
349
359
  def get_template_path(agent_name: str, debug: bool = False) -> pathlib.Path:
350
360
  """Get the absolute path to the agent template directory."""
351
361
  current_dir = pathlib.Path(__file__).parent.parent.parent.parent
352
- template_path = current_dir / "agents" / agent_name / "template"
362
+ template_path = current_dir / "agents" / agent_name / ".template"
353
363
  if debug:
354
364
  logging.debug(f"Looking for template in: {template_path}")
355
365
  logging.debug(f"Template exists: {template_path.exists()}")
@@ -397,6 +407,8 @@ def process_template(
397
407
  datastore: str | None = None,
398
408
  session_type: str | None = None,
399
409
  output_dir: pathlib.Path | None = None,
410
+ remote_template_path: pathlib.Path | None = None,
411
+ remote_config: dict[str, Any] | None = None,
400
412
  ) -> None:
401
413
  """Process the template directory and create a new project.
402
414
 
@@ -409,14 +421,30 @@ def process_template(
409
421
  datastore: Optional datastore type for data ingestion
410
422
  session_type: Optional session type for cloud_run deployment
411
423
  output_dir: Optional output directory path, defaults to current directory
424
+ remote_template_path: Optional path to remote template for overlay
425
+ remote_config: Optional remote template configuration
412
426
  """
413
427
  logging.debug(f"Processing template from {template_dir}")
414
428
  logging.debug(f"Project name: {project_name}")
415
429
  logging.debug(f"Include pipeline: {datastore}")
416
430
  logging.debug(f"Output directory: {output_dir}")
417
431
 
418
- # Get paths
419
- agent_path = template_dir.parent # Get parent of template dir
432
+ # Handle remote vs local templates
433
+ is_remote = remote_template_path is not None
434
+
435
+ if is_remote:
436
+ # For remote templates, determine the base template
437
+ base_template_name = get_base_template_name(remote_config or {})
438
+ agent_path = (
439
+ pathlib.Path(__file__).parent.parent.parent.parent
440
+ / "agents"
441
+ / base_template_name
442
+ )
443
+ logging.debug(f"Remote template using base: {base_template_name}")
444
+ else:
445
+ # For local templates, use the existing logic
446
+ agent_path = template_dir.parent # Get parent of template dir
447
+
420
448
  logging.debug(f"agent path: {agent_path}")
421
449
  logging.debug(f"agent path exists: {agent_path.exists()}")
422
450
  logging.debug(
@@ -489,7 +517,7 @@ def process_template(
489
517
  copy_frontend_files(frontend_type, project_template)
490
518
  logging.debug(f"4. Processed frontend files for type: {frontend_type}")
491
519
 
492
- # 5. Finally, copy agent-specific files to override everything else
520
+ # 5. Copy agent-specific files to override base template
493
521
  if agent_path.exists():
494
522
  for folder in OVERWRITE_FOLDERS:
495
523
  agent_folder = agent_path / folder
@@ -500,11 +528,27 @@ def process_template(
500
528
  agent_folder, project_folder, agent_name, overwrite=True
501
529
  )
502
530
 
531
+ # 6. Finally, overlay remote template files if present
532
+ if is_remote and remote_template_path:
533
+ logging.debug(
534
+ f"6. Overlaying remote template files from {remote_template_path}"
535
+ )
536
+ copy_files(
537
+ remote_template_path,
538
+ project_template,
539
+ agent_name=agent_name,
540
+ overwrite=True,
541
+ )
542
+
503
543
  # Load and validate template config first
504
- template_path = pathlib.Path(template_dir)
505
- config = load_template_config(template_path)
544
+ if is_remote:
545
+ config = remote_config or {}
546
+ else:
547
+ template_path = pathlib.Path(template_dir)
548
+ config = load_template_config(template_path)
549
+
506
550
  if not config:
507
- raise ValueError(f"Could not load template config from {template_path}")
551
+ raise ValueError("Could not load template config")
508
552
 
509
553
  # Validate deployment target
510
554
  available_targets = config.get("settings", {}).get("deployment_targets", [])
@@ -516,8 +560,8 @@ def process_template(
516
560
  f"Invalid deployment target '{deployment_target}'. Available targets: {available_targets}"
517
561
  )
518
562
 
519
- # Load template config
520
- template_config = load_template_config(pathlib.Path(template_dir))
563
+ # Use the already loaded config
564
+ template_config = config
521
565
 
522
566
  # Check if data processing should be included
523
567
  if include_data_ingestion and datastore:
@@ -625,9 +669,36 @@ def process_template(
625
669
  file_path.unlink()
626
670
  logging.debug(f"Deleted {file_path}")
627
671
 
628
- # After copying template files, handle the lock file
629
- if deployment_target:
630
- # Get the source lock file path
672
+ # Handle pyproject.toml and uv.lock files
673
+ if is_remote and remote_template_path:
674
+ # For remote templates, use their pyproject.toml and uv.lock if they exist
675
+ remote_pyproject = remote_template_path / "pyproject.toml"
676
+ remote_uv_lock = remote_template_path / "uv.lock"
677
+
678
+ if remote_pyproject.exists():
679
+ shutil.copy2(
680
+ remote_pyproject, final_destination / "pyproject.toml"
681
+ )
682
+ logging.debug("Used pyproject.toml from remote template")
683
+
684
+ if remote_uv_lock.exists():
685
+ shutil.copy2(remote_uv_lock, final_destination / "uv.lock")
686
+ 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
+ elif deployment_target:
701
+ # For local templates, use the existing logic
631
702
  lock_path = (
632
703
  pathlib.Path(__file__).parent.parent.parent.parent
633
704
  / "src"
@@ -706,8 +777,12 @@ def copy_files(
706
777
  return True
707
778
  if "__pycache__" in str(path) or path.name == "__pycache__":
708
779
  return True
780
+ if ".git" in path.parts:
781
+ return True
709
782
  if agent_name is not None and should_exclude_path(path, agent_name):
710
783
  return True
784
+ if path.is_dir() and path.name == ".template":
785
+ return True
711
786
  return False
712
787
 
713
788
  if src.is_dir():