agent-starter-pack 0.7.1__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.
- {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/METADATA +7 -6
- {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/RECORD +63 -59
- agents/README.md +7 -0
- agents/adk_base/{template/.templateconfig.yaml → .template/templateconfig.yaml} +3 -1
- agents/adk_base/notebooks/adk_app_testing.ipynb +8 -6
- agents/adk_gemini_fullstack/{template/.templateconfig.yaml → .template/templateconfig.yaml} +3 -2
- agents/adk_gemini_fullstack/notebooks/adk_app_testing.ipynb +8 -6
- agents/agentic_rag/{template/.templateconfig.yaml → .template/templateconfig.yaml} +2 -1
- agents/agentic_rag/notebooks/adk_app_testing.ipynb +8 -6
- agents/crewai_coding_crew/{template/.templateconfig.yaml → .template/templateconfig.yaml} +1 -1
- llm.txt +7 -0
- src/base_template/Makefile +5 -1
- src/base_template/README.md +2 -2
- src/base_template/deployment/cd/deploy-to-prod.yaml +1 -16
- src/base_template/deployment/cd/staging.yaml +4 -19
- src/base_template/deployment/terraform/apis.tf +2 -2
- src/base_template/deployment/terraform/build_triggers.tf +5 -5
- src/base_template/deployment/terraform/dev/apis.tf +8 -1
- src/base_template/deployment/terraform/dev/variables.tf +3 -1
- src/base_template/deployment/terraform/iam.tf +8 -8
- src/base_template/deployment/terraform/locals.tf +9 -2
- src/base_template/deployment/terraform/log_sinks.tf +2 -2
- src/base_template/deployment/terraform/service_accounts.tf +3 -3
- src/base_template/deployment/terraform/storage.tf +7 -7
- src/base_template/deployment/terraform/variables.tf +3 -0
- src/base_template/pyproject.toml +4 -3
- src/cli/commands/create.py +191 -41
- src/cli/commands/list.py +158 -0
- src/cli/commands/setup_cicd.py +2 -2
- src/cli/main.py +2 -0
- src/cli/utils/cicd.py +2 -2
- src/cli/utils/remote_template.py +254 -0
- src/cli/utils/template.py +134 -25
- src/deployment_targets/agent_engine/app/agent_engine_app.py +7 -7
- src/deployment_targets/agent_engine/tests/load_test/README.md +1 -6
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +13 -3
- src/deployment_targets/cloud_run/Dockerfile +3 -0
- src/deployment_targets/cloud_run/app/server.py +18 -0
- src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +231 -0
- src/deployment_targets/cloud_run/deployment/terraform/service.tf +360 -0
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +8 -5
- src/deployment_targets/cloud_run/tests/load_test/README.md +2 -2
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +21 -17
- src/frontends/adk_gemini_fullstack/frontend/src/App.tsx +2 -3
- src/resources/docs/adk-cheatsheet.md +1 -1
- src/resources/locks/uv-adk_base-agent_engine.lock +873 -236
- src/resources/locks/uv-adk_base-cloud_run.lock +1169 -283
- src/resources/locks/uv-adk_gemini_fullstack-agent_engine.lock +873 -236
- src/resources/locks/uv-adk_gemini_fullstack-cloud_run.lock +1169 -283
- src/resources/locks/uv-agentic_rag-agent_engine.lock +508 -373
- src/resources/locks/uv-agentic_rag-cloud_run.lock +668 -469
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +582 -587
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +791 -733
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +587 -478
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +799 -627
- src/resources/locks/uv-live_api-cloud_run.lock +803 -603
- src/resources/setup_cicd/github.tf +2 -2
- src/utils/lock_utils.py +1 -1
- src/deployment_targets/cloud_run/uv.lock +0 -6952
- {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/entry_points.txt +0 -0
- {agent_starter_pack-0.7.1.dist-info → agent_starter_pack-0.9.0.dist-info}/licenses/LICENSE +0 -0
- /agents/langgraph_base_react/{template/.templateconfig.yaml → .template/templateconfig.yaml} +0 -0
- /agents/live_api/{template/.templateconfig.yaml → .template/templateconfig.yaml} +0 -0
src/cli/commands/list.py
ADDED
@@ -0,0 +1,158 @@
|
|
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 pathlib
|
17
|
+
|
18
|
+
import click
|
19
|
+
import yaml
|
20
|
+
from rich.console import Console
|
21
|
+
from rich.table import Table
|
22
|
+
|
23
|
+
from ..utils.remote_template import fetch_remote_template, parse_agent_spec
|
24
|
+
from ..utils.template import get_available_agents
|
25
|
+
|
26
|
+
console = Console()
|
27
|
+
|
28
|
+
|
29
|
+
def display_agents_from_path(base_path: pathlib.Path, source_name: str) -> None:
|
30
|
+
"""Scans a directory and displays available agents."""
|
31
|
+
table = Table(
|
32
|
+
title=f"Available agents in [bold blue]{source_name}[/]",
|
33
|
+
show_header=True,
|
34
|
+
header_style="bold magenta",
|
35
|
+
)
|
36
|
+
table.add_column("Name", style="bold")
|
37
|
+
table.add_column("Path", style="cyan")
|
38
|
+
table.add_column("Description", style="dim")
|
39
|
+
|
40
|
+
if not base_path.is_dir():
|
41
|
+
console.print(f"Directory not found: {base_path}", style="bold red")
|
42
|
+
return
|
43
|
+
|
44
|
+
found_agents = False
|
45
|
+
# Search for templateconfig.yaml files to identify agents
|
46
|
+
for config_path in sorted(base_path.glob("**/templateconfig.yaml")):
|
47
|
+
try:
|
48
|
+
with open(config_path) as f:
|
49
|
+
config = yaml.safe_load(f)
|
50
|
+
|
51
|
+
agent_name = config.get("name", config_path.parent.parent.name)
|
52
|
+
description = config.get("description", "No description.")
|
53
|
+
|
54
|
+
# Display the agent's path relative to the scanned directory
|
55
|
+
relative_path = config_path.parent.parent.relative_to(base_path)
|
56
|
+
|
57
|
+
table.add_row(agent_name, f"/{relative_path}", description)
|
58
|
+
found_agents = True
|
59
|
+
|
60
|
+
except Exception as e:
|
61
|
+
logging.warning(
|
62
|
+
f"Could not load agent from {config_path.parent.parent}: {e}"
|
63
|
+
)
|
64
|
+
|
65
|
+
if not found_agents:
|
66
|
+
console.print(f"No agents found in {source_name}", style="yellow")
|
67
|
+
else:
|
68
|
+
console.print(table)
|
69
|
+
|
70
|
+
|
71
|
+
def list_remote_agents(remote_source: str, scan_from_root: bool = False) -> None:
|
72
|
+
"""Lists agents from a remote source (Git URL)."""
|
73
|
+
spec = parse_agent_spec(remote_source)
|
74
|
+
if not spec:
|
75
|
+
console.print(f"Invalid remote source: {remote_source}", style="bold red")
|
76
|
+
return
|
77
|
+
|
78
|
+
console.print(f"\nFetching agents from [bold blue]{remote_source}[/]...")
|
79
|
+
|
80
|
+
try:
|
81
|
+
# fetch_remote_template clones the repo and returns a path to the
|
82
|
+
# specific template directory within the repo.
|
83
|
+
template_dir_path = fetch_remote_template(spec)
|
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
|
94
|
+
|
95
|
+
display_agents_from_path(scan_path, remote_source)
|
96
|
+
|
97
|
+
except (RuntimeError, FileNotFoundError) as e:
|
98
|
+
console.print(f"Error: {e}", style="bold red")
|
99
|
+
|
100
|
+
|
101
|
+
@click.command("list")
|
102
|
+
@click.option(
|
103
|
+
"--adk",
|
104
|
+
is_flag=True,
|
105
|
+
help="List agents from the official google/adk-samples repository.",
|
106
|
+
)
|
107
|
+
@click.option(
|
108
|
+
"--source",
|
109
|
+
"-s",
|
110
|
+
help="List agents from a local path or a remote Git URL.",
|
111
|
+
)
|
112
|
+
def list_agents(adk: bool, source: str | None) -> None:
|
113
|
+
"""
|
114
|
+
Lists available agent templates.
|
115
|
+
|
116
|
+
Defaults to listing built-in agents if no options are provided.
|
117
|
+
"""
|
118
|
+
if adk and source:
|
119
|
+
console.print(
|
120
|
+
"Error: --adk and --source are mutually exclusive.", style="bold red"
|
121
|
+
)
|
122
|
+
return
|
123
|
+
|
124
|
+
if adk:
|
125
|
+
list_remote_agents("https://github.com/google/adk-samples", scan_from_root=True)
|
126
|
+
return
|
127
|
+
|
128
|
+
if source:
|
129
|
+
source_path = pathlib.Path(source)
|
130
|
+
if source_path.is_dir():
|
131
|
+
display_agents_from_path(source_path, f"local directory '{source}'")
|
132
|
+
elif parse_agent_spec(source):
|
133
|
+
list_remote_agents(source)
|
134
|
+
else:
|
135
|
+
console.print(
|
136
|
+
f"Error: Source '{source}' is not a valid local directory or remote URL.",
|
137
|
+
style="bold red",
|
138
|
+
)
|
139
|
+
return
|
140
|
+
|
141
|
+
# Default behavior: list built-in agents
|
142
|
+
agents = get_available_agents()
|
143
|
+
if not agents:
|
144
|
+
console.print("No built-in agents found.", style="yellow")
|
145
|
+
return
|
146
|
+
|
147
|
+
table = Table(
|
148
|
+
title="Available built-in agents",
|
149
|
+
show_header=True,
|
150
|
+
header_style="bold magenta",
|
151
|
+
)
|
152
|
+
table.add_column("Number", style="dim", width=12)
|
153
|
+
table.add_column("Name", style="bold")
|
154
|
+
table.add_column("Description")
|
155
|
+
|
156
|
+
for i, (_, agent) in enumerate(agents.items()):
|
157
|
+
table.add_row(str(i + 1), agent["name"], agent["description"])
|
158
|
+
console.print(table)
|
src/cli/commands/setup_cicd.py
CHANGED
@@ -168,8 +168,8 @@ def update_build_triggers(tf_dir: Path) -> None:
|
|
168
168
|
|
169
169
|
# Add repository dependency to all trigger resources
|
170
170
|
modified_content = content.replace(
|
171
|
-
"depends_on = [resource.google_project_service.cicd_services, resource.google_project_service.
|
172
|
-
"depends_on = [resource.google_project_service.cicd_services, resource.google_project_service.
|
171
|
+
"depends_on = [resource.google_project_service.cicd_services, resource.google_project_service.deploy_project_services]",
|
172
|
+
"depends_on = [resource.google_project_service.cicd_services, resource.google_project_service.deploy_project_services, google_cloudbuildv2_repository.repo]",
|
173
173
|
)
|
174
174
|
|
175
175
|
# Update repository reference in all triggers
|
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__":
|
src/cli/utils/cicd.py
CHANGED
@@ -228,9 +228,9 @@ def create_github_connection(
|
|
228
228
|
"\n🔑 Authentication Required:", style="bold yellow"
|
229
229
|
)
|
230
230
|
console.print(
|
231
|
-
f"Please visit [link={action_uri}]this page[/link] to authenticate Cloud Build with GitHub:"
|
231
|
+
f"Please visit [link={action_uri}][bold blue]this page[/bold blue][/link] to authenticate Cloud Build with GitHub:"
|
232
232
|
)
|
233
|
-
|
233
|
+
print(f"\n{action_uri}\n")
|
234
234
|
console.print(
|
235
235
|
"(Copy and paste the link into your browser if clicking doesn't work)"
|
236
236
|
)
|
@@ -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)
|