agent-starter-pack 0.18.1__py3-none-any.whl → 0.19.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.
Potentially problematic release.
This version of agent-starter-pack might be problematic. Click here for more details.
- agent_starter_pack/agents/adk_a2a_base/.template/templateconfig.yaml +22 -0
- agent_starter_pack/agents/adk_a2a_base/README.md +22 -0
- agent_starter_pack/agents/adk_a2a_base/app/__init__.py +17 -0
- agent_starter_pack/agents/adk_a2a_base/app/agent.py +70 -0
- agent_starter_pack/agents/adk_a2a_base/notebooks/adk_a2a_app_testing.ipynb +600 -0
- agent_starter_pack/agents/adk_a2a_base/notebooks/evaluating_adk_agent.ipynb +1535 -0
- agent_starter_pack/agents/adk_a2a_base/tests/integration/test_agent.py +58 -0
- agent_starter_pack/base_template/.gitignore +1 -1
- agent_starter_pack/base_template/Makefile +11 -11
- agent_starter_pack/base_template/README.md +1 -1
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +10 -2
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +26 -5
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +18 -3
- agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +34 -3
- agent_starter_pack/cli/utils/cicd.py +20 -4
- agent_starter_pack/cli/utils/register_gemini_enterprise.py +79 -84
- agent_starter_pack/cli/utils/template.py +2 -0
- agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +104 -2
- agent_starter_pack/deployment_targets/agent_engine/tests/load_test/load_test.py +144 -0
- agent_starter_pack/deployment_targets/agent_engine/tests/{% if cookiecutter.is_adk_a2a %}helpers.py{% else %}unused_helpers.py{% endif %} +138 -0
- agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +88 -4
- agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +4 -0
- agent_starter_pack/deployment_targets/cloud_run/Dockerfile +3 -0
- agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +7 -0
- agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/service.tf +16 -2
- agent_starter_pack/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +218 -1
- agent_starter_pack/deployment_targets/cloud_run/tests/load_test/README.md +2 -2
- agent_starter_pack/deployment_targets/cloud_run/tests/load_test/load_test.py +51 -4
- agent_starter_pack/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +66 -0
- agent_starter_pack/resources/locks/uv-adk_a2a_base-agent_engine.lock +4224 -0
- agent_starter_pack/resources/locks/uv-adk_a2a_base-cloud_run.lock +4819 -0
- agent_starter_pack/resources/locks/uv-adk_base-agent_engine.lock +230 -236
- agent_starter_pack/resources/locks/uv-adk_base-cloud_run.lock +290 -296
- agent_starter_pack/resources/locks/uv-adk_live-agent_engine.lock +230 -236
- agent_starter_pack/resources/locks/uv-adk_live-cloud_run.lock +290 -296
- agent_starter_pack/resources/locks/uv-agentic_rag-agent_engine.lock +234 -239
- agent_starter_pack/resources/locks/uv-agentic_rag-cloud_run.lock +294 -299
- agent_starter_pack/resources/locks/uv-crewai_coding_crew-agent_engine.lock +221 -228
- agent_starter_pack/resources/locks/uv-crewai_coding_crew-cloud_run.lock +279 -286
- agent_starter_pack/resources/locks/uv-langgraph_base_react-agent_engine.lock +226 -233
- agent_starter_pack/resources/locks/uv-langgraph_base_react-cloud_run.lock +298 -305
- {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/METADATA +2 -1
- {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/RECORD +46 -36
- {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/entry_points.txt +0 -0
- {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/licenses/LICENSE +0 -0
agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py
CHANGED
|
@@ -213,18 +213,52 @@ def test_feedback_endpoint(server_fixture: subprocess.Popen[str]) -> None:
|
|
|
213
213
|
logger.info("Feedback endpoint test passed")
|
|
214
214
|
{% else %}
|
|
215
215
|
|
|
216
|
+
# mypy: disable-error-code="arg-type"
|
|
217
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
218
|
+
|
|
219
|
+
import os
|
|
220
|
+
|
|
221
|
+
import pytest
|
|
222
|
+
import pytest_asyncio
|
|
223
|
+
from google.adk.artifacts import InMemoryArtifactService
|
|
224
|
+
from google.adk.sessions import InMemorySessionService
|
|
225
|
+
|
|
226
|
+
from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
|
|
227
|
+
from tests.helpers import (
|
|
228
|
+
build_get_request,
|
|
229
|
+
build_post_request,
|
|
230
|
+
poll_task_completion,
|
|
231
|
+
)
|
|
232
|
+
{%- elif cookiecutter.is_adk %}
|
|
233
|
+
|
|
216
234
|
import logging
|
|
217
235
|
|
|
218
236
|
import pytest
|
|
219
|
-
{%- if cookiecutter.is_adk %}
|
|
220
237
|
from google.adk.events.event import Event
|
|
221
238
|
|
|
222
239
|
from {{cookiecutter.agent_directory}}.agent import root_agent
|
|
223
240
|
from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
|
|
224
241
|
{%- else %}
|
|
225
242
|
|
|
243
|
+
import logging
|
|
244
|
+
|
|
245
|
+
import pytest
|
|
246
|
+
|
|
226
247
|
from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
|
|
227
248
|
{%- endif %}
|
|
249
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@pytest_asyncio.fixture
|
|
253
|
+
async def agent_app() -> AgentEngineApp:
|
|
254
|
+
"""Fixture to create and set up AgentEngineApp instance"""
|
|
255
|
+
app = await AgentEngineApp.create(
|
|
256
|
+
artifact_service_builder=lambda: InMemoryArtifactService(),
|
|
257
|
+
session_service_builder=lambda: InMemorySessionService(),
|
|
258
|
+
)
|
|
259
|
+
app.set_up()
|
|
260
|
+
return app
|
|
261
|
+
{%- else %}
|
|
228
262
|
|
|
229
263
|
|
|
230
264
|
@pytest.fixture
|
|
@@ -237,8 +271,75 @@ def agent_app() -> AgentEngineApp:
|
|
|
237
271
|
{%- endif %}
|
|
238
272
|
app.set_up()
|
|
239
273
|
return app
|
|
274
|
+
{% endif %}
|
|
275
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@pytest.mark.asyncio
|
|
279
|
+
async def test_agent_on_message_send(agent_app: AgentEngineApp) -> None:
|
|
280
|
+
"""Test complete A2A message workflow from send to task completion with artifacts."""
|
|
281
|
+
# Send message
|
|
282
|
+
message_data = {
|
|
283
|
+
"message": {
|
|
284
|
+
"messageId": f"msg-{os.urandom(8).hex()}",
|
|
285
|
+
"content": [{"text": "What is the capital of France?"}],
|
|
286
|
+
"role": "ROLE_USER",
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
response = await agent_app.on_message_send(
|
|
290
|
+
request=build_post_request(message_data),
|
|
291
|
+
context=None,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Verify task creation
|
|
295
|
+
assert "task" in response and "id" in response["task"], (
|
|
296
|
+
"Expected task with ID in response"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Poll for completion
|
|
300
|
+
final_response = await poll_task_completion(agent_app, response["task"]["id"])
|
|
301
|
+
|
|
302
|
+
# Verify artifacts
|
|
303
|
+
assert final_response.get("artifacts"), "Expected artifacts in completed task"
|
|
304
|
+
artifact = final_response["artifacts"][0]
|
|
305
|
+
assert artifact.get("parts") and artifact["parts"][0].get("text"), (
|
|
306
|
+
"Expected artifact with text content"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@pytest.mark.asyncio
|
|
311
|
+
async def test_agent_card(agent_app: AgentEngineApp) -> None:
|
|
312
|
+
"""Test agent card retrieval and validation of required A2A fields."""
|
|
313
|
+
response = await agent_app.handle_authenticated_agent_card(
|
|
314
|
+
request=build_get_request(None),
|
|
315
|
+
context=None,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Verify core agent card fields
|
|
319
|
+
assert response.get("name") == "root_agent", "Expected agent name 'root_agent'"
|
|
320
|
+
assert response.get("protocolVersion") == "0.3.0", "Expected protocol version 0.3.0"
|
|
321
|
+
assert response.get("preferredTransport") == "HTTP+JSON", (
|
|
322
|
+
"Expected HTTP+JSON transport"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Verify capabilities
|
|
326
|
+
capabilities = response.get("capabilities", {})
|
|
327
|
+
assert capabilities.get("streaming") is False, "Expected streaming disabled"
|
|
328
|
+
|
|
329
|
+
# Verify skills
|
|
330
|
+
skills = response.get("skills", [])
|
|
331
|
+
assert len(skills) > 0, "Expected at least one skill"
|
|
332
|
+
for skill in skills:
|
|
333
|
+
assert all(key in skill for key in ["id", "name", "description"]), (
|
|
334
|
+
"Expected id, name, and description in each skill"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Verify extended card support
|
|
338
|
+
assert response.get("supportsAuthenticatedExtendedCard") is True, (
|
|
339
|
+
"Expected supportsAuthenticatedExtendedCard to be True"
|
|
340
|
+
)
|
|
341
|
+
{% elif cookiecutter.is_adk %}
|
|
240
342
|
|
|
241
|
-
{% if cookiecutter.is_adk %}
|
|
242
343
|
@pytest.mark.asyncio
|
|
243
344
|
async def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
|
|
244
345
|
"""
|
|
@@ -293,6 +394,7 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
|
|
|
293
394
|
|
|
294
395
|
logging.info("All assertions passed for agent feedback test")
|
|
295
396
|
{% else %}
|
|
397
|
+
|
|
296
398
|
def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
|
|
297
399
|
"""
|
|
298
400
|
Integration test for the agent stream query functionality.
|
|
@@ -139,6 +139,149 @@ class RemoteAgentUser(WebSocketUser):
|
|
|
139
139
|
# Set the host via command line: locust -f load_test.py --host=https://your-deployed-service.run.app
|
|
140
140
|
host = "http://localhost:8000" # Default for local testing
|
|
141
141
|
{%- else %}
|
|
142
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
143
|
+
|
|
144
|
+
import json
|
|
145
|
+
import logging
|
|
146
|
+
import os
|
|
147
|
+
import time
|
|
148
|
+
|
|
149
|
+
from locust import HttpUser, between, task
|
|
150
|
+
|
|
151
|
+
# Configure logging
|
|
152
|
+
logging.basicConfig(
|
|
153
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
154
|
+
)
|
|
155
|
+
logger = logging.getLogger(__name__)
|
|
156
|
+
|
|
157
|
+
# Initialize Vertex AI and load agent config
|
|
158
|
+
with open("deployment_metadata.json") as f:
|
|
159
|
+
remote_agent_engine_id = json.load(f)["remote_agent_engine_id"]
|
|
160
|
+
|
|
161
|
+
parts = remote_agent_engine_id.split("/")
|
|
162
|
+
project_id = parts[1]
|
|
163
|
+
location = parts[3]
|
|
164
|
+
engine_id = parts[5]
|
|
165
|
+
|
|
166
|
+
# Convert remote agent engine ID to URLs
|
|
167
|
+
base_url = f"https://{location}-aiplatform.googleapis.com"
|
|
168
|
+
a2a_base_path = f"/v1beta1/projects/{project_id}/locations/{location}/reasoningEngines/{engine_id}/a2a/v1"
|
|
169
|
+
|
|
170
|
+
logger.info("Using remote agent engine ID: %s", remote_agent_engine_id)
|
|
171
|
+
logger.info("Using base URL: %s", base_url)
|
|
172
|
+
logger.info("Using API base path: %s", a2a_base_path)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SendMessageUser(HttpUser):
|
|
176
|
+
"""Simulates a user interacting with the send message API."""
|
|
177
|
+
|
|
178
|
+
wait_time = between(1, 3) # Wait 1-3 seconds between tasks
|
|
179
|
+
host = base_url # Set the base host URL for Locust
|
|
180
|
+
|
|
181
|
+
@task
|
|
182
|
+
def send_message_and_poll(self) -> None:
|
|
183
|
+
"""Simulates a chat interaction: sends a message and polls for completion."""
|
|
184
|
+
headers = {"Content-Type": "application/json"}
|
|
185
|
+
headers["Authorization"] = f"Bearer {os.environ['_AUTH_TOKEN']}"
|
|
186
|
+
|
|
187
|
+
data = {
|
|
188
|
+
"message": {
|
|
189
|
+
"messageId": "msg-id",
|
|
190
|
+
"content": [{"text": "Hello! What's the weather in New York?"}],
|
|
191
|
+
"role": "ROLE_USER",
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
e2e_start_time = time.time()
|
|
196
|
+
with self.client.post(
|
|
197
|
+
f"{a2a_base_path}/message:send",
|
|
198
|
+
headers=headers,
|
|
199
|
+
json=data,
|
|
200
|
+
catch_response=True,
|
|
201
|
+
name="/v1/message:send",
|
|
202
|
+
) as response:
|
|
203
|
+
if response.status_code != 200:
|
|
204
|
+
response.failure(
|
|
205
|
+
f"Send failed with status code: {response.status_code}"
|
|
206
|
+
)
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
response.success()
|
|
210
|
+
response_data = response.json()
|
|
211
|
+
|
|
212
|
+
# Extract task ID
|
|
213
|
+
try:
|
|
214
|
+
task_id = response_data["task"]["id"]
|
|
215
|
+
except (KeyError, TypeError) as e:
|
|
216
|
+
logger.error(f"Failed to extract task ID: {e}")
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
# Poll for task completion
|
|
220
|
+
max_polls = 20 # Maximum number of poll attempts
|
|
221
|
+
poll_interval = 0.5 # Seconds between polls
|
|
222
|
+
poll_count = 0
|
|
223
|
+
|
|
224
|
+
while poll_count < max_polls:
|
|
225
|
+
poll_count += 1
|
|
226
|
+
time.sleep(poll_interval)
|
|
227
|
+
|
|
228
|
+
with self.client.get(
|
|
229
|
+
f"{a2a_base_path}/tasks/{task_id}",
|
|
230
|
+
headers=headers,
|
|
231
|
+
catch_response=True,
|
|
232
|
+
name="/v1/tasks/{id}",
|
|
233
|
+
) as poll_response:
|
|
234
|
+
if poll_response.status_code != 200:
|
|
235
|
+
poll_response.failure(
|
|
236
|
+
f"Poll failed with status code: {poll_response.status_code}"
|
|
237
|
+
)
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
poll_data = poll_response.json()
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
task_state = poll_data["status"]["state"]
|
|
244
|
+
except (KeyError, TypeError) as e:
|
|
245
|
+
logger.error(f"Failed to extract task state: {e}")
|
|
246
|
+
poll_response.failure(f"Invalid response format: {e}")
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Check if task is complete
|
|
250
|
+
if task_state in ["TASK_STATE_COMPLETED"]:
|
|
251
|
+
poll_response.success()
|
|
252
|
+
|
|
253
|
+
# Measure end-to-end time
|
|
254
|
+
e2e_duration = (time.time() - e2e_start_time) * 1000
|
|
255
|
+
|
|
256
|
+
# Fire custom event for end-to-end metrics
|
|
257
|
+
self.environment.events.request.fire(
|
|
258
|
+
request_type="E2E",
|
|
259
|
+
name="message:send_and_complete",
|
|
260
|
+
response_time=e2e_duration,
|
|
261
|
+
response_length=len(json.dumps(poll_data)),
|
|
262
|
+
response=poll_response,
|
|
263
|
+
context={"poll_count": poll_count},
|
|
264
|
+
)
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
elif task_state in ["TASK_STATE_WORKING"]:
|
|
268
|
+
poll_response.success()
|
|
269
|
+
|
|
270
|
+
else:
|
|
271
|
+
poll_response.failure(f"Task failed with state: {task_state}")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
# Timeout - task didn't complete in time
|
|
275
|
+
self.environment.events.request.fire(
|
|
276
|
+
request_type="TIMEOUT",
|
|
277
|
+
name="message:timeout",
|
|
278
|
+
response_time=(time.time() - e2e_start_time) * 1000,
|
|
279
|
+
response_length=0,
|
|
280
|
+
response=None,
|
|
281
|
+
context={"poll_count": poll_count},
|
|
282
|
+
exception=TimeoutError(f"Task did not complete after {max_polls} polls"),
|
|
283
|
+
)
|
|
284
|
+
{%- else %}
|
|
142
285
|
|
|
143
286
|
import json
|
|
144
287
|
import logging
|
|
@@ -253,3 +396,4 @@ class ChatStreamUser(HttpUser):
|
|
|
253
396
|
else:
|
|
254
397
|
response.failure(f"Unexpected status code: {response.status_code}")
|
|
255
398
|
{%- endif %}
|
|
399
|
+
{%- endif %}
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
# mypy: disable-error-code="arg-type"
|
|
16
|
+
|
|
17
|
+
"""Helper functions for testing AgentEngineApp with A2A protocol."""
|
|
18
|
+
|
|
19
|
+
import asyncio
|
|
20
|
+
import json
|
|
21
|
+
from collections.abc import Awaitable, Callable
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
from starlette.requests import Request
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
|
|
28
|
+
|
|
29
|
+
# Test constants
|
|
30
|
+
POLL_MAX_ATTEMPTS = 30
|
|
31
|
+
POLL_INTERVAL_SECONDS = 1.0
|
|
32
|
+
TEST_ARTIFACTS_BUCKET = "test-artifacts-bucket"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def receive_wrapper(data: dict[str, Any] | None) -> Callable[[], Awaitable[dict]]:
|
|
36
|
+
"""Creates a mock ASGI receive callable for testing.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
data: Dictionary to encode as JSON request body
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Async callable that returns mock ASGI receive message
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
async def receive() -> dict:
|
|
46
|
+
byte_data = json.dumps(data).encode("utf-8")
|
|
47
|
+
return {"type": "http.request", "body": byte_data, "more_body": False}
|
|
48
|
+
|
|
49
|
+
return receive
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_post_request(
|
|
53
|
+
data: dict[str, Any] | None = None, path_params: dict[str, str] | None = None
|
|
54
|
+
) -> Request:
|
|
55
|
+
"""Builds a mock Starlette Request object for a POST request with JSON data.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data: JSON data to include in request body
|
|
59
|
+
path_params: Path parameters to include in request scope
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Mock Starlette Request object
|
|
63
|
+
"""
|
|
64
|
+
scope: dict[str, Any] = {
|
|
65
|
+
"type": "http",
|
|
66
|
+
"http_version": "1.1",
|
|
67
|
+
"headers": [(b"content-type", b"application/json")],
|
|
68
|
+
"app": None,
|
|
69
|
+
}
|
|
70
|
+
if path_params:
|
|
71
|
+
scope["path_params"] = path_params
|
|
72
|
+
receiver = receive_wrapper(data)
|
|
73
|
+
return Request(scope, receiver)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_get_request(path_params: dict[str, str] | None) -> Request:
|
|
77
|
+
"""Builds a mock Starlette Request object for a GET request.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
path_params: Path parameters to include in request scope
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Mock Starlette Request object
|
|
84
|
+
"""
|
|
85
|
+
scope: dict[str, Any] = {
|
|
86
|
+
"type": "http",
|
|
87
|
+
"http_version": "1.1",
|
|
88
|
+
"query_string": b"",
|
|
89
|
+
"app": None,
|
|
90
|
+
}
|
|
91
|
+
if path_params:
|
|
92
|
+
scope["path_params"] = path_params
|
|
93
|
+
|
|
94
|
+
async def receive() -> dict:
|
|
95
|
+
return {"type": "http.disconnect"}
|
|
96
|
+
|
|
97
|
+
return Request(scope, receive)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def poll_task_completion(
|
|
101
|
+
agent_app: "AgentEngineApp",
|
|
102
|
+
task_id: str,
|
|
103
|
+
max_attempts: int = POLL_MAX_ATTEMPTS,
|
|
104
|
+
interval: float = POLL_INTERVAL_SECONDS,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""Poll for task completion and return final response.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
agent_app: The AgentEngineApp instance to poll
|
|
110
|
+
task_id: The task ID to poll for
|
|
111
|
+
max_attempts: Maximum number of polling attempts
|
|
112
|
+
interval: Seconds to wait between polls
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Final task response when completed
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
AssertionError: If task fails or times out
|
|
119
|
+
"""
|
|
120
|
+
for _ in range(max_attempts):
|
|
121
|
+
poll_request = build_get_request({"id": task_id})
|
|
122
|
+
response = await agent_app.on_get_task(
|
|
123
|
+
request=poll_request,
|
|
124
|
+
context=None,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
task_state = response.get("status", {}).get("state", "")
|
|
128
|
+
|
|
129
|
+
if task_state == "TASK_STATE_COMPLETED":
|
|
130
|
+
return response
|
|
131
|
+
elif task_state == "TASK_STATE_FAILED":
|
|
132
|
+
raise AssertionError(f"Task failed: {response}")
|
|
133
|
+
|
|
134
|
+
await asyncio.sleep(interval)
|
|
135
|
+
|
|
136
|
+
raise AssertionError(
|
|
137
|
+
f"Task did not complete within {max_attempts * interval} seconds"
|
|
138
|
+
)
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
# mypy: disable-error-code="attr-defined,arg-type"
|
|
16
16
|
{%- if cookiecutter.is_adk %}
|
|
17
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
18
|
+
import asyncio
|
|
19
|
+
import copy
|
|
20
|
+
{%- endif %}
|
|
17
21
|
import logging
|
|
18
22
|
import os
|
|
19
23
|
from typing import Any
|
|
@@ -21,18 +25,35 @@ from typing import Any
|
|
|
21
25
|
import click
|
|
22
26
|
import google.auth
|
|
23
27
|
import vertexai
|
|
28
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
29
|
+
from a2a.types import AgentCapabilities, AgentCard, TransportProtocol
|
|
30
|
+
from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
|
|
31
|
+
from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder
|
|
32
|
+
from google.adk.apps.app import App
|
|
33
|
+
{%- endif %}
|
|
24
34
|
from google.adk.artifacts import GcsArtifactService
|
|
35
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
36
|
+
from google.adk.runners import Runner
|
|
37
|
+
from google.adk.sessions import InMemorySessionService
|
|
38
|
+
{%- endif %}
|
|
25
39
|
from google.cloud import logging as google_cloud_logging
|
|
26
40
|
from opentelemetry import trace
|
|
27
41
|
from opentelemetry.sdk.trace import TracerProvider, export
|
|
28
42
|
from vertexai._genai.types import AgentEngine, AgentEngineConfig{%- if cookiecutter.is_adk_live %}, AgentServerMode{%- endif %}
|
|
29
43
|
{%- if cookiecutter.is_adk_live %}
|
|
30
44
|
from vertexai.preview.reasoning_engines import AdkApp
|
|
45
|
+
{%- elif cookiecutter.is_adk_a2a %}
|
|
46
|
+
from vertexai.preview.reasoning_engines import A2aAgent
|
|
31
47
|
{%- else %}
|
|
32
48
|
from vertexai.agent_engines.templates.adk import AdkApp
|
|
33
49
|
{%- endif %}
|
|
50
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
51
|
+
|
|
52
|
+
from {{cookiecutter.agent_directory}}.agent import app
|
|
53
|
+
{%- else %}
|
|
34
54
|
|
|
35
55
|
from {{cookiecutter.agent_directory}}.agent import root_agent
|
|
56
|
+
{%- endif %}
|
|
36
57
|
from {{cookiecutter.agent_directory}}.utils.deployment import (
|
|
37
58
|
parse_env_vars,
|
|
38
59
|
print_deployment_success,
|
|
@@ -41,9 +62,53 @@ from {{cookiecutter.agent_directory}}.utils.deployment import (
|
|
|
41
62
|
from {{cookiecutter.agent_directory}}.utils.gcs import create_bucket_if_not_exists
|
|
42
63
|
from {{cookiecutter.agent_directory}}.utils.tracing import CloudTraceLoggingSpanExporter
|
|
43
64
|
from {{cookiecutter.agent_directory}}.utils.typing import Feedback
|
|
65
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AgentEngineApp(A2aAgent):
|
|
69
|
+
@staticmethod
|
|
70
|
+
async def create(
|
|
71
|
+
artifact_service_builder: Any = None,
|
|
72
|
+
session_service_builder: Any = None,
|
|
73
|
+
) -> Any:
|
|
74
|
+
"""Create an AgentEngineApp instance."""
|
|
75
|
+
|
|
76
|
+
def create_runner() -> Runner:
|
|
77
|
+
"""Create a Runner for the AgentEngineApp."""
|
|
78
|
+
return Runner(
|
|
79
|
+
app=app,
|
|
80
|
+
session_service=session_service_builder()
|
|
81
|
+
if session_service_builder
|
|
82
|
+
else None,
|
|
83
|
+
artifact_service=artifact_service_builder()
|
|
84
|
+
if artifact_service_builder
|
|
85
|
+
else None,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return AgentEngineApp(
|
|
89
|
+
agent_executor_builder=lambda: A2aAgentExecutor(runner=create_runner()),
|
|
90
|
+
agent_card=await AgentEngineApp.build_agent_card(app=app),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
async def build_agent_card(app: App) -> AgentCard:
|
|
95
|
+
"""Builds the Agent Card dynamically from the app."""
|
|
96
|
+
agent_card_builder = AgentCardBuilder(
|
|
97
|
+
agent=app.root_agent,
|
|
98
|
+
# Agent Engine does not support streaming yet
|
|
99
|
+
capabilities=AgentCapabilities(streaming=False),
|
|
100
|
+
rpc_url="http://localhost:9999/",
|
|
101
|
+
agent_version=os.getenv("AGENT_VERSION", "0.1.0"),
|
|
102
|
+
)
|
|
103
|
+
agent_card = await agent_card_builder.build()
|
|
104
|
+
agent_card.preferred_transport = TransportProtocol.http_json # Http Only.
|
|
105
|
+
agent_card.supports_authenticated_extended_card = True
|
|
106
|
+
return agent_card
|
|
107
|
+
{% else %}
|
|
44
108
|
|
|
45
109
|
|
|
46
110
|
class AgentEngineApp(AdkApp):
|
|
111
|
+
{%- endif %}
|
|
47
112
|
def set_up(self) -> None:
|
|
48
113
|
"""Set up logging and tracing for the agent engine app."""
|
|
49
114
|
import logging
|
|
@@ -74,6 +139,16 @@ class AgentEngineApp(AdkApp):
|
|
|
74
139
|
operations = super().register_operations()
|
|
75
140
|
operations[""] = operations.get("", []) + ["register_feedback"]
|
|
76
141
|
return operations
|
|
142
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
143
|
+
|
|
144
|
+
def clone(self) -> "AgentEngineApp":
|
|
145
|
+
"""Returns a clone of the Agent Engine application."""
|
|
146
|
+
template_attributes = self._tmpl_attrs
|
|
147
|
+
return self.__class__(
|
|
148
|
+
agent_card=copy.deepcopy(self.agent_card),
|
|
149
|
+
agent_executor_builder=self._tmpl_attrs.get("agent_executor_builder"),
|
|
150
|
+
)
|
|
151
|
+
{%- endif %}
|
|
77
152
|
|
|
78
153
|
{%- else %}
|
|
79
154
|
import logging
|
|
@@ -272,7 +347,7 @@ def deploy_agent_engine_app(
|
|
|
272
347
|
if not staging_bucket_uri:
|
|
273
348
|
staging_bucket_uri = f"gs://{project}-agent-engine"
|
|
274
349
|
if not artifacts_bucket_name:
|
|
275
|
-
artifacts_bucket_name = f"
|
|
350
|
+
artifacts_bucket_name = f"{project}-agent-engine"
|
|
276
351
|
|
|
277
352
|
{%- if "adk" in cookiecutter.tags %}
|
|
278
353
|
create_bucket_if_not_exists(
|
|
@@ -303,16 +378,25 @@ def deploy_agent_engine_app(
|
|
|
303
378
|
# Read requirements
|
|
304
379
|
with open(requirements_file) as f:
|
|
305
380
|
requirements = f.read().strip().split("\n")
|
|
306
|
-
{
|
|
381
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
382
|
+
agent_engine = asyncio.run(
|
|
383
|
+
AgentEngineApp.create(
|
|
384
|
+
artifact_service_builder=lambda: GcsArtifactService(
|
|
385
|
+
bucket_name=artifacts_bucket_name
|
|
386
|
+
),
|
|
387
|
+
session_service_builder=lambda: InMemorySessionService(),
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
{%- elif cookiecutter.is_adk %}
|
|
307
391
|
agent_engine = AgentEngineApp(
|
|
308
392
|
agent=root_agent,
|
|
309
393
|
artifact_service_builder=lambda: GcsArtifactService(
|
|
310
394
|
bucket_name=artifacts_bucket_name
|
|
311
395
|
),
|
|
312
396
|
)
|
|
313
|
-
{
|
|
397
|
+
{%- else %}
|
|
314
398
|
agent_engine = AgentEngineApp(project_id=project)
|
|
315
|
-
{
|
|
399
|
+
{%- endif %}
|
|
316
400
|
# Set worker parallelism to 1
|
|
317
401
|
env_vars["NUM_WORKERS"] = "1"
|
|
318
402
|
|
|
@@ -80,6 +80,10 @@ def print_deployment_success(
|
|
|
80
80
|
{%- if cookiecutter.is_adk %}
|
|
81
81
|
{%- if cookiecutter.is_adk_live %}
|
|
82
82
|
print("\n✅ Deployment successful! Run your agent with: `make playground-remote`")
|
|
83
|
+
{%- elif cookiecutter.is_adk_a2a %}
|
|
84
|
+
print(
|
|
85
|
+
"\n✅ Deployment successful! Test your agent: notebooks/adk_a2a_app_testing.ipynb"
|
|
86
|
+
)
|
|
83
87
|
{%- else %}
|
|
84
88
|
print(
|
|
85
89
|
"\n✅ Deployment successful! Test your agent: notebooks/adk_app_testing.ipynb"
|
|
@@ -43,6 +43,9 @@ RUN uv sync --frozen
|
|
|
43
43
|
ARG COMMIT_SHA=""
|
|
44
44
|
ENV COMMIT_SHA=${COMMIT_SHA}
|
|
45
45
|
|
|
46
|
+
ARG AGENT_VERSION=0.0.0
|
|
47
|
+
ENV AGENT_VERSION=${AGENT_VERSION}
|
|
48
|
+
|
|
46
49
|
EXPOSE 8080
|
|
47
50
|
|
|
48
51
|
CMD ["uv", "run", "uvicorn", "{{cookiecutter.agent_directory}}.server:app", "--host", "0.0.0.0", "--port", "8080"]
|
|
@@ -150,6 +150,13 @@ resource "google_cloud_run_v2_service" "app" {
|
|
|
150
150
|
containers {
|
|
151
151
|
image = "us-docker.pkg.dev/cloudrun/container/hello"
|
|
152
152
|
|
|
153
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
154
|
+
env {
|
|
155
|
+
name = "APP_URL"
|
|
156
|
+
value = "https://${var.project_name}-${data.google_project.project.number}.${var.region}.run.app"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
{%- endif %}
|
|
153
160
|
resources {
|
|
154
161
|
limits = {
|
|
155
162
|
cpu = "4"
|
|
@@ -148,7 +148,7 @@ resource "google_alloydb_user" "db_user" {
|
|
|
148
148
|
|
|
149
149
|
{%- endif %}
|
|
150
150
|
|
|
151
|
-
resource "google_cloud_run_v2_service" "app_staging" {
|
|
151
|
+
resource "google_cloud_run_v2_service" "app_staging" {
|
|
152
152
|
name = var.project_name
|
|
153
153
|
location = var.region
|
|
154
154
|
project = var.staging_project_id
|
|
@@ -172,6 +172,13 @@ resource "google_cloud_run_v2_service" "app_staging" {
|
|
|
172
172
|
# Placeholder, will be replaced by the CI/CD pipeline
|
|
173
173
|
image = "us-docker.pkg.dev/cloudrun/container/hello"
|
|
174
174
|
|
|
175
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
176
|
+
env {
|
|
177
|
+
name = "APP_URL"
|
|
178
|
+
value = "https://${var.project_name}-${data.google_project.project["staging"].number}.${var.region}.run.app"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
{%- endif %}
|
|
175
182
|
resources {
|
|
176
183
|
limits = {
|
|
177
184
|
cpu = "4"
|
|
@@ -266,7 +273,7 @@ resource "google_cloud_run_v2_service" "app_staging" {
|
|
|
266
273
|
depends_on = [google_project_service.deploy_project_services]
|
|
267
274
|
}
|
|
268
275
|
|
|
269
|
-
resource "google_cloud_run_v2_service" "app_prod" {
|
|
276
|
+
resource "google_cloud_run_v2_service" "app_prod" {
|
|
270
277
|
name = var.project_name
|
|
271
278
|
location = var.region
|
|
272
279
|
project = var.prod_project_id
|
|
@@ -290,6 +297,13 @@ resource "google_cloud_run_v2_service" "app_prod" {
|
|
|
290
297
|
# Placeholder, will be replaced by the CI/CD pipeline
|
|
291
298
|
image = "us-docker.pkg.dev/cloudrun/container/hello"
|
|
292
299
|
|
|
300
|
+
{%- if cookiecutter.is_adk_a2a %}
|
|
301
|
+
env {
|
|
302
|
+
name = "APP_URL"
|
|
303
|
+
value = "https://${var.project_name}-${data.google_project.project["prod"].number}.${var.region}.run.app"
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
{%- endif %}
|
|
293
307
|
resources {
|
|
294
308
|
limits = {
|
|
295
309
|
cpu = "4"
|