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