agent-starter-pack 0.14.0__py3-none-any.whl → 0.15.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 (29) hide show
  1. {agent_starter_pack-0.14.0.dist-info → agent_starter_pack-0.15.0.dist-info}/METADATA +4 -3
  2. {agent_starter_pack-0.14.0.dist-info → agent_starter_pack-0.15.0.dist-info}/RECORD +29 -29
  3. agents/adk_base/notebooks/adk_app_testing.ipynb +36 -18
  4. agents/agentic_rag/notebooks/adk_app_testing.ipynb +36 -18
  5. src/base_template/Makefile +1 -1
  6. src/base_template/README.md +1 -1
  7. src/base_template/deployment/terraform/vars/env.tfvars +3 -4
  8. src/base_template/pyproject.toml +4 -4
  9. src/cli/commands/create.py +34 -11
  10. src/cli/utils/gcp.py +19 -0
  11. src/cli/utils/remote_template.py +144 -1
  12. src/cli/utils/template.py +12 -0
  13. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +6 -3
  14. src/deployment_targets/agent_engine/tests/load_test/load_test.py +12 -3
  15. src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +45 -38
  16. src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +12 -0
  17. src/deployment_targets/cloud_run/deployment/terraform/service.tf +24 -0
  18. src/resources/locks/uv-adk_base-agent_engine.lock +1738 -1703
  19. src/resources/locks/uv-adk_base-cloud_run.lock +2295 -2253
  20. src/resources/locks/uv-agentic_rag-agent_engine.lock +1971 -1914
  21. src/resources/locks/uv-agentic_rag-cloud_run.lock +2605 -2537
  22. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +2839 -2807
  23. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +3755 -3714
  24. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +2441 -2396
  25. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +3243 -3185
  26. src/resources/locks/uv-live_api-cloud_run.lock +2950 -2902
  27. {agent_starter_pack-0.14.0.dist-info → agent_starter_pack-0.15.0.dist-info}/WHEEL +0 -0
  28. {agent_starter_pack-0.14.0.dist-info → agent_starter_pack-0.15.0.dist-info}/entry_points.txt +0 -0
  29. {agent_starter_pack-0.14.0.dist-info → agent_starter_pack-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -28,6 +28,7 @@ if sys.version_info >= (3, 11):
28
28
  else:
29
29
  import tomli as tomllib
30
30
  from jinja2 import Environment
31
+ from packaging import version as pkg_version
31
32
  from rich.console import Console
32
33
 
33
34
 
@@ -127,15 +128,112 @@ def parse_agent_spec(agent_spec: str) -> RemoteTemplateSpec | None:
127
128
  return None
128
129
 
129
130
 
131
+ def check_and_execute_with_version_lock(
132
+ template_dir: pathlib.Path,
133
+ original_agent_spec: str | None = None,
134
+ locked: bool = False,
135
+ ) -> bool:
136
+ """Check if remote template has agent-starter-pack version lock and execute if found.
137
+
138
+ Args:
139
+ template_dir: Path to the fetched template directory
140
+ original_agent_spec: The original agent spec (remote URL) to replace with local path
141
+ locked: Whether this is already a locked execution (prevents recursion)
142
+
143
+ Returns:
144
+ True if version lock was found and executed, False otherwise
145
+ """
146
+ # Skip version locking if we're already in a locked execution (prevents recursion)
147
+ if locked:
148
+ return False
149
+ uv_lock_path = template_dir / "uv.lock"
150
+ version = parse_agent_starter_pack_version_from_lock(uv_lock_path)
151
+
152
+ if version:
153
+ console = Console()
154
+ console.print(
155
+ f"🔒 Remote template requires agent-starter-pack version {version}",
156
+ style="bold blue",
157
+ )
158
+ console.print(
159
+ f"📦 Switching to version {version}...",
160
+ style="dim",
161
+ )
162
+
163
+ # Reconstruct the original command but with version constraint
164
+ import sys
165
+
166
+ original_args = sys.argv[1:] # Skip 'agent-starter-pack' or script name
167
+
168
+ # Add version lock specific parameters and handle remote URL replacement
169
+ if original_agent_spec:
170
+ # Replace remote agent spec with local path
171
+ modified_args = []
172
+ for arg in original_args:
173
+ if arg == original_agent_spec:
174
+ # Replace remote URL with local template directory
175
+ modified_args.append(f"local@{template_dir}")
176
+ else:
177
+ modified_args.append(arg)
178
+ original_args = modified_args
179
+
180
+ # Add version lock flags only for ASP versions 0.14.1 and above
181
+ if pkg_version.parse(version) > pkg_version.parse("0.14.1"):
182
+ original_args.extend(["--skip-welcome", "--locked"])
183
+
184
+ try:
185
+ # Check if uvx is available
186
+ subprocess.run(["uvx", "--version"], capture_output=True, check=True)
187
+ except (subprocess.CalledProcessError, FileNotFoundError):
188
+ console.print(
189
+ f"❌ Remote template requires agent-starter-pack version {version}, but 'uvx' is not installed",
190
+ style="bold red",
191
+ )
192
+ console.print(
193
+ "💡 Install uv to use version-locked remote templates:",
194
+ style="bold blue",
195
+ )
196
+ console.print(" curl -LsSf https://astral.sh/uv/install.sh | sh")
197
+ console.print(
198
+ " OR visit: https://docs.astral.sh/uv/getting-started/installation/"
199
+ )
200
+ sys.exit(1)
201
+
202
+ try:
203
+ # Execute uvx with the locked version
204
+ cmd = ["uvx", f"agent-starter-pack=={version}", *original_args]
205
+ logging.debug(f"Executing nested command: {' '.join(cmd)}")
206
+ subprocess.run(cmd, check=True)
207
+ return True
208
+
209
+ except subprocess.CalledProcessError as e:
210
+ console.print(
211
+ f"❌ Failed to execute with locked version {version}: {e}",
212
+ style="bold red",
213
+ )
214
+ console.print(
215
+ "⚠️ Continuing with current version, but compatibility is not guaranteed",
216
+ style="yellow",
217
+ )
218
+ # Continue with current execution instead of failing completely
219
+
220
+ return False
221
+
222
+
130
223
  def fetch_remote_template(
131
224
  spec: RemoteTemplateSpec,
225
+ original_agent_spec: str | None = None,
226
+ locked: bool = False,
132
227
  ) -> tuple[pathlib.Path, pathlib.Path]:
133
228
  """Fetch remote template and return path to template directory.
134
229
 
135
- Uses Git to clone the remote repository.
230
+ Uses Git to clone the remote repository. If the template contains a uv.lock
231
+ with agent-starter-pack version constraint, will execute nested uvx command.
136
232
 
137
233
  Args:
138
234
  spec: Remote template specification
235
+ original_agent_spec: Original agent spec string (used to prevent recursion)
236
+ locked: Whether this is already a locked execution (prevents recursion)
139
237
 
140
238
  Returns:
141
239
  A tuple containing:
@@ -188,6 +286,16 @@ def fetch_remote_template(
188
286
  f"Template path not found in the repository: {spec.template_path}"
189
287
  )
190
288
 
289
+ # Check for version lock and execute nested command if found
290
+ if check_and_execute_with_version_lock(
291
+ template_dir, original_agent_spec, locked
292
+ ):
293
+ # If we executed with locked version, the nested process will handle everything
294
+ # Clean up and exit successfully
295
+ shutil.rmtree(temp_path, ignore_errors=True)
296
+ # Exit with success since the nested command will handle the rest
297
+ sys.exit(0)
298
+
191
299
  return template_dir, temp_path
192
300
  except Exception as e:
193
301
  # Clean up on error
@@ -458,6 +566,41 @@ def display_adk_caveat_if_needed(agents: dict[int, dict[str, Any]]) -> None:
458
566
  )
459
567
 
460
568
 
569
+ def parse_agent_starter_pack_version_from_lock(
570
+ uv_lock_path: pathlib.Path,
571
+ ) -> str | None:
572
+ """Parse agent-starter-pack version from uv.lock file.
573
+
574
+ Args:
575
+ uv_lock_path: Path to uv.lock file
576
+
577
+ Returns:
578
+ Version string if found, None otherwise
579
+ """
580
+ if not uv_lock_path.exists():
581
+ return None
582
+
583
+ try:
584
+ with open(uv_lock_path, "rb") as f:
585
+ lock_data = tomllib.load(f)
586
+
587
+ # Look for agent-starter-pack in the packages section
588
+ packages = lock_data.get("package", [])
589
+ for package in packages:
590
+ if package.get("name") == "agent-starter-pack":
591
+ version = package.get("version")
592
+ if version:
593
+ logging.debug(
594
+ f"Found agent-starter-pack version {version} in uv.lock"
595
+ )
596
+ return version
597
+
598
+ except Exception as e:
599
+ logging.warning(f"Error parsing uv.lock file {uv_lock_path}: {e}")
600
+
601
+ return None
602
+
603
+
461
604
  def render_and_merge_makefiles(
462
605
  base_template_path: pathlib.Path,
463
606
  final_destination: pathlib.Path,
src/cli/utils/template.py CHANGED
@@ -449,6 +449,7 @@ def process_template(
449
449
  in_folder: bool = False,
450
450
  cli_overrides: dict[str, Any] | None = None,
451
451
  agent_garden: bool = False,
452
+ remote_spec: Any | None = None,
452
453
  ) -> None:
453
454
  """Process the template directory and create a new project.
454
455
 
@@ -526,6 +527,15 @@ def process_template(
526
527
  try:
527
528
  os.chdir(temp_path) # Change to temp directory
528
529
 
530
+ # Extract agent sample info for labeling when using agent garden with remote templates
531
+ agent_sample_id = None
532
+ agent_sample_publisher = None
533
+ if agent_garden and remote_spec and remote_spec.is_adk_samples:
534
+ # For ADK samples, template_path is like "python/agents/sample-name"
535
+ agent_sample_id = pathlib.Path(remote_spec.template_path).name
536
+ # For ADK samples, publisher is always "google"
537
+ agent_sample_publisher = "google"
538
+
529
539
  # Create the cookiecutter template structure
530
540
  cookiecutter_template = temp_path / "template"
531
541
  cookiecutter_template.mkdir(parents=True)
@@ -706,6 +716,8 @@ def process_template(
706
716
  "datastore_type": datastore if datastore else "",
707
717
  "agent_directory": get_agent_directory(template_config, cli_overrides),
708
718
  "agent_garden": agent_garden,
719
+ "agent_sample_id": agent_sample_id or "",
720
+ "agent_sample_publisher": agent_sample_publisher or "",
709
721
  "adk_cheatsheet": adk_cheatsheet_content,
710
722
  "llm_txt": llm_txt_content,
711
723
  "_copy_without_render": [
@@ -38,14 +38,17 @@ def agent_app() -> AgentEngineApp:
38
38
  return app
39
39
 
40
40
  {% if "adk" in cookiecutter.tags %}
41
- def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
41
+ @pytest.mark.asyncio
42
+ async def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
42
43
  """
43
44
  Integration test for the agent stream query functionality.
44
45
  Tests that the agent returns valid streaming responses.
45
46
  """
46
- # Create message and events for the stream_query
47
+ # Create message and events for the async_stream_query
47
48
  message = "What's the weather in San Francisco?"
48
- events = list(agent_app.stream_query(message=message, user_id="test"))
49
+ events = []
50
+ async for event in agent_app.async_stream_query(message=message, user_id="test"):
51
+ events.append(event)
49
52
  assert len(events) > 0, "Expected at least one chunk in response"
50
53
 
51
54
  # Check for valid content in the response
@@ -36,7 +36,7 @@ engine_id = parts[5]
36
36
 
37
37
  # Convert remote agent engine ID to streaming URL.
38
38
  base_url = f"https://{location}-aiplatform.googleapis.com"
39
- url_path = f"/v1beta1/projects/{project_id}/locations/{location}/reasoningEngines/{engine_id}:streamQuery"
39
+ url_path = f"/v1/projects/{project_id}/locations/{location}/reasoningEngines/{engine_id}:streamQuery"
40
40
 
41
41
  logger.info("Using remote agent engine ID: %s", remote_agent_engine_id)
42
42
  logger.info("Using base URL: %s", base_url)
@@ -56,10 +56,11 @@ class ChatStreamUser(HttpUser):
56
56
  headers["Authorization"] = f"Bearer {os.environ['_AUTH_TOKEN']}"
57
57
  {% if "adk" in cookiecutter.tags %}
58
58
  data = {
59
+ "class_method": "async_stream_query",
59
60
  "input": {
60
- "message": "What's the weather in San Francisco?",
61
61
  "user_id": "test",
62
- }
62
+ "message": "What's the weather in San Francisco?",
63
+ },
63
64
  }
64
65
  {% else %}
65
66
  data = {
@@ -83,7 +84,11 @@ class ChatStreamUser(HttpUser):
83
84
  headers=headers,
84
85
  json=data,
85
86
  catch_response=True,
87
+ {%- if "adk" in cookiecutter.tags %}
88
+ name="/streamQuery async_stream_query",
89
+ {%- else %}
86
90
  name="/stream_messages first message",
91
+ {%- endif %}
87
92
  stream=True,
88
93
  params={"alt": "sse"},
89
94
  ) as response:
@@ -107,7 +112,11 @@ class ChatStreamUser(HttpUser):
107
112
  total_time = end_time - start_time
108
113
  self.environment.events.request.fire(
109
114
  request_type="POST",
115
+ {%- if "adk" in cookiecutter.tags %}
116
+ name="/streamQuery end",
117
+ {%- else %}
110
118
  name="/stream_messages end",
119
+ {%- endif %}
111
120
  response_time=total_time * 1000, # Convert to milliseconds
112
121
  response_length=len(events),
113
122
  response=response,
@@ -14,7 +14,6 @@
14
14
 
15
15
  # mypy: disable-error-code="attr-defined,arg-type"
16
16
  {%- if "adk" in cookiecutter.tags %}
17
- import copy
18
17
  import datetime
19
18
  import json
20
19
  import logging
@@ -27,8 +26,8 @@ from google.adk.artifacts import GcsArtifactService
27
26
  from google.cloud import logging as google_cloud_logging
28
27
  from opentelemetry import trace
29
28
  from opentelemetry.sdk.trace import TracerProvider, export
30
- from vertexai import agent_engines
31
- from vertexai.preview.reasoning_engines import AdkApp
29
+ from vertexai._genai.types import AgentEngine, AgentEngineConfig
30
+ from vertexai.agent_engines.templates.adk import AdkApp
32
31
 
33
32
  from {{cookiecutter.agent_directory}}.agent import root_agent
34
33
  from {{cookiecutter.agent_directory}}.utils.gcs import create_bucket_if_not_exists
@@ -39,7 +38,10 @@ from {{cookiecutter.agent_directory}}.utils.typing import Feedback
39
38
  class AgentEngineApp(AdkApp):
40
39
  def set_up(self) -> None:
41
40
  """Set up logging and tracing for the agent engine app."""
41
+ import logging
42
+
42
43
  super().set_up()
44
+ logging.basicConfig(level=logging.INFO)
43
45
  logging_client = google_cloud_logging.Client()
44
46
  self.logger = logging_client.logger(__name__)
45
47
  provider = TracerProvider()
@@ -62,22 +64,9 @@ class AgentEngineApp(AdkApp):
62
64
  Extends the base operations to include feedback registration functionality.
63
65
  """
64
66
  operations = super().register_operations()
65
- operations[""] = operations[""] + ["register_feedback"]
67
+ operations[""] = operations.get("", []) + ["register_feedback"]
66
68
  return operations
67
69
 
68
- def clone(self) -> "AgentEngineApp":
69
- """Returns a clone of the ADK application."""
70
- template_attributes = self._tmpl_attrs
71
-
72
- return self.__class__(
73
- agent=copy.deepcopy(template_attributes["agent"]),
74
- enable_tracing=bool(template_attributes.get("enable_tracing", False)),
75
- session_service_builder=template_attributes.get("session_service_builder"),
76
- artifact_service_builder=template_attributes.get(
77
- "artifact_service_builder"
78
- ),
79
- env_vars=template_attributes.get("env_vars"),
80
- )
81
70
  {%- else %}
82
71
  import datetime
83
72
  import json
@@ -93,7 +82,7 @@ import vertexai
93
82
  from google.cloud import logging as google_cloud_logging
94
83
  from langchain_core.runnables import RunnableConfig
95
84
  from traceloop.sdk import Instruments, Traceloop
96
- from vertexai import agent_engines
85
+ from vertexai._genai.types import AgentEngine, AgentEngineConfig
97
86
 
98
87
  from {{cookiecutter.agent_directory}}.utils.gcs import create_bucket_if_not_exists
99
88
  from {{cookiecutter.agent_directory}}.utils.tracing import CloudTraceLoggingSpanExporter
@@ -209,9 +198,9 @@ def deploy_agent_engine_app(
209
198
  extra_packages: list[str] = ["./{{cookiecutter.agent_directory}}"],
210
199
  env_vars: dict[str, str] = {},
211
200
  service_account: str | None = None,
212
- ) -> agent_engines.AgentEngine:
201
+ ) -> AgentEngine:
213
202
  """Deploy the agent engine app to Vertex AI."""
214
-
203
+ logging.basicConfig(level=logging.INFO)
215
204
  staging_bucket_uri = f"gs://{project}-agent-engine"
216
205
  {%- if "adk" in cookiecutter.tags %}
217
206
  artifacts_bucket_name = f"{project}-{{cookiecutter.project_name}}-logs-data"
@@ -223,7 +212,12 @@ def deploy_agent_engine_app(
223
212
  bucket_name=staging_bucket_uri, project=project, location=location
224
213
  )
225
214
 
226
- vertexai.init(project=project, location=location, staging_bucket=staging_bucket_uri)
215
+ # Initialize vertexai client
216
+ client = vertexai.Client(
217
+ project=project,
218
+ location=location,
219
+ )
220
+ vertexai.init(project=project, location=location)
227
221
 
228
222
  # Read requirements
229
223
  with open(requirements_file) as f:
@@ -242,38 +236,51 @@ def deploy_agent_engine_app(
242
236
  env_vars["NUM_WORKERS"] = "1"
243
237
 
244
238
  # Common configuration for both create and update operations
239
+ config = AgentEngineConfig(
240
+ display_name=agent_name,
241
+ description="{{cookiecutter.agent_description}}",
242
+ extra_packages=extra_packages,
243
+ env_vars=env_vars,
244
+ service_account=service_account,
245
+ requirements=requirements,
246
+ staging_bucket=staging_bucket_uri,
247
+ )
248
+
245
249
  agent_config = {
246
- "agent_engine": agent_engine,
247
- "display_name": agent_name,
248
- "description": "{{cookiecutter.agent_description}}",
249
- "extra_packages": extra_packages,
250
- "env_vars": env_vars,
251
- "service_account": service_account,
250
+ "agent": agent_engine,
251
+ "config": config,
252
252
  }
253
253
  logging.info(f"Agent config: {agent_config}")
254
- agent_config["requirements"] = requirements
255
254
 
256
255
  # Check if an agent with this name already exists
257
- existing_agents = list(agent_engines.list(filter=f"display_name={agent_name}"))
258
- if existing_agents:
256
+ existing_agents = list(client.agent_engines.list())
257
+ matching_agents = [
258
+ agent
259
+ for agent in existing_agents
260
+ if agent.api_resource.display_name == agent_name
261
+ ]
262
+
263
+ if matching_agents:
259
264
  # Update the existing agent with new configuration
260
265
  logging.info(f"Updating existing agent: {agent_name}")
261
- remote_agent = existing_agents[0].update(**agent_config)
266
+ remote_agent = client.agent_engines.update(
267
+ name=matching_agents[0].api_resource.name, **agent_config
268
+ )
262
269
  else:
263
270
  # Create a new agent if none exists
264
271
  logging.info(f"Creating new agent: {agent_name}")
265
- remote_agent = agent_engines.create(**agent_config)
272
+ remote_agent = client.agent_engines.create(**agent_config)
266
273
 
267
- config = {
268
- "remote_agent_engine_id": remote_agent.resource_name,
274
+ metadata = {
275
+ "remote_agent_engine_id": remote_agent.api_resource.name,
269
276
  "deployment_timestamp": datetime.datetime.now().isoformat(),
270
277
  }
271
- config_file = "deployment_metadata.json"
278
+ metadata_file = "deployment_metadata.json"
272
279
 
273
- with open(config_file, "w") as f:
274
- json.dump(config, f, indent=2)
280
+ with open(metadata_file, "w") as f:
281
+ json.dump(metadata, f, indent=2)
275
282
 
276
- logging.info(f"Agent Engine ID written to {config_file}")
283
+ logging.info(f"Agent Engine ID written to {metadata_file}")
277
284
 
278
285
  return remote_agent
279
286
 
@@ -133,6 +133,18 @@ resource "google_cloud_run_v2_service" "app" {
133
133
  project = var.dev_project_id
134
134
  deletion_protection = false
135
135
  ingress = "INGRESS_TRAFFIC_ALL"
136
+ labels = {
137
+ {%- if "adk" in cookiecutter.tags %}
138
+ "created-by" = "adk"
139
+ {%- endif %}
140
+ {%- if cookiecutter.agent_garden %}
141
+ "deployed-with" = "agent-garden"
142
+ {%- if cookiecutter.agent_sample_id %}
143
+ "vertex-agent-sample-id" = "{{cookiecutter.agent_sample_id}}"
144
+ "vertex-agent-sample-publisher" = "{{cookiecutter.agent_sample_publisher}}"
145
+ {%- endif %}
146
+ {%- endif %}
147
+ }
136
148
 
137
149
  template {
138
150
  containers {
@@ -154,6 +154,18 @@ resource "google_cloud_run_v2_service" "app_staging" {
154
154
  project = var.staging_project_id
155
155
  deletion_protection = false
156
156
  ingress = "INGRESS_TRAFFIC_ALL"
157
+ labels = {
158
+ {%- if "adk" in cookiecutter.tags %}
159
+ "created-by" = "adk"
160
+ {%- endif %}
161
+ {%- if cookiecutter.agent_garden %}
162
+ "deployed-with" = "agent-garden"
163
+ {%- if cookiecutter.agent_sample_id %}
164
+ "vertex-agent-sample-id" = "{{cookiecutter.agent_sample_id}}"
165
+ "vertex-agent-sample-publisher" = "{{cookiecutter.agent_sample_publisher}}"
166
+ {%- endif %}
167
+ {%- endif %}
168
+ }
157
169
 
158
170
  template {
159
171
  containers {
@@ -260,6 +272,18 @@ resource "google_cloud_run_v2_service" "app_prod" {
260
272
  project = var.prod_project_id
261
273
  deletion_protection = false
262
274
  ingress = "INGRESS_TRAFFIC_ALL"
275
+ labels = {
276
+ {%- if "adk" in cookiecutter.tags %}
277
+ "created-by" = "adk"
278
+ {%- endif %}
279
+ {%- if cookiecutter.agent_garden %}
280
+ "deployed-with" = "agent-garden"
281
+ {%- if cookiecutter.agent_sample_id %}
282
+ "vertex-agent-sample-id" = "{{cookiecutter.agent_sample_id}}"
283
+ "vertex-agent-sample-publisher" = "{{cookiecutter.agent_sample_publisher}}"
284
+ {%- endif %}
285
+ {%- endif %}
286
+ }
263
287
 
264
288
  template {
265
289
  containers {