agent-starter-pack 0.18.2__py3-none-any.whl → 0.21.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 (114) hide show
  1. agent_starter_pack/agents/{langgraph_base_react → adk_a2a_base}/.template/templateconfig.yaml +5 -12
  2. agent_starter_pack/agents/adk_a2a_base/README.md +37 -0
  3. agent_starter_pack/{frontends/streamlit/frontend/style/app_markdown.py → agents/adk_a2a_base/app/__init__.py} +3 -23
  4. agent_starter_pack/agents/adk_a2a_base/app/agent.py +70 -0
  5. agent_starter_pack/agents/adk_a2a_base/notebooks/adk_a2a_app_testing.ipynb +583 -0
  6. agent_starter_pack/agents/{crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb → adk_a2a_base/notebooks/evaluating_adk_agent.ipynb} +163 -199
  7. agent_starter_pack/agents/adk_a2a_base/tests/integration/test_agent.py +58 -0
  8. agent_starter_pack/agents/adk_base/app/__init__.py +2 -2
  9. agent_starter_pack/agents/adk_base/app/agent.py +3 -0
  10. agent_starter_pack/agents/adk_base/notebooks/adk_app_testing.ipynb +13 -28
  11. agent_starter_pack/agents/adk_live/app/__init__.py +17 -0
  12. agent_starter_pack/agents/adk_live/app/agent.py +3 -0
  13. agent_starter_pack/agents/agentic_rag/app/__init__.py +2 -2
  14. agent_starter_pack/agents/agentic_rag/app/agent.py +3 -0
  15. agent_starter_pack/agents/agentic_rag/notebooks/adk_app_testing.ipynb +13 -28
  16. agent_starter_pack/agents/{crewai_coding_crew → langgraph_base}/.template/templateconfig.yaml +12 -9
  17. agent_starter_pack/agents/langgraph_base/README.md +30 -0
  18. agent_starter_pack/agents/langgraph_base/app/__init__.py +17 -0
  19. agent_starter_pack/agents/{langgraph_base_react → langgraph_base}/app/agent.py +4 -4
  20. agent_starter_pack/agents/{langgraph_base_react → langgraph_base}/tests/integration/test_agent.py +1 -1
  21. agent_starter_pack/base_template/.gitignore +4 -2
  22. agent_starter_pack/base_template/Makefile +110 -16
  23. agent_starter_pack/base_template/README.md +97 -12
  24. agent_starter_pack/base_template/deployment/terraform/dev/apis.tf +4 -6
  25. agent_starter_pack/base_template/deployment/terraform/dev/providers.tf +5 -1
  26. agent_starter_pack/base_template/deployment/terraform/dev/variables.tf +5 -3
  27. agent_starter_pack/base_template/deployment/terraform/dev/{% if cookiecutter.is_adk %}telemetry.tf{% else %}unused_telemetry.tf{% endif %} +193 -0
  28. agent_starter_pack/base_template/deployment/terraform/github.tf +16 -9
  29. agent_starter_pack/base_template/deployment/terraform/locals.tf +7 -7
  30. agent_starter_pack/base_template/deployment/terraform/providers.tf +5 -1
  31. agent_starter_pack/base_template/deployment/terraform/sql/completions.sql +138 -0
  32. agent_starter_pack/base_template/deployment/terraform/storage.tf +0 -9
  33. agent_starter_pack/base_template/deployment/terraform/variables.tf +15 -19
  34. agent_starter_pack/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} +20 -22
  35. agent_starter_pack/base_template/deployment/terraform/{% if cookiecutter.is_adk %}telemetry.tf{% else %}unused_telemetry.tf{% endif %} +206 -0
  36. agent_starter_pack/base_template/pyproject.toml +5 -17
  37. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +19 -4
  38. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +36 -11
  39. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +24 -5
  40. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +44 -9
  41. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/telemetry.py +96 -0
  42. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/{utils → app_utils}/typing.py +4 -6
  43. agent_starter_pack/{agents/crewai_coding_crew/app/crew/config/agents.yaml → base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}converters{% else %}unused_converters{% endif %}/__init__.py } +9 -23
  44. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}converters{% else %}unused_converters{% endif %}/part_converter.py +138 -0
  45. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/__init__.py +13 -0
  46. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/a2a_agent_executor.py +265 -0
  47. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/app_utils/{% if cookiecutter.is_a2a and cookiecutter.agent_name == 'langgraph_base' %}executor{% else %}unused_executor{% endif %}/task_result_aggregator.py +152 -0
  48. agent_starter_pack/cli/commands/create.py +40 -4
  49. agent_starter_pack/cli/commands/enhance.py +1 -1
  50. agent_starter_pack/cli/commands/register_gemini_enterprise.py +1070 -0
  51. agent_starter_pack/cli/main.py +2 -0
  52. agent_starter_pack/cli/utils/cicd.py +20 -4
  53. agent_starter_pack/cli/utils/template.py +257 -25
  54. agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +113 -16
  55. agent_starter_pack/deployment_targets/agent_engine/tests/load_test/README.md +2 -2
  56. agent_starter_pack/deployment_targets/agent_engine/tests/load_test/load_test.py +178 -9
  57. agent_starter_pack/deployment_targets/agent_engine/tests/{% if cookiecutter.is_a2a %}helpers.py{% else %}unused_helpers.py{% endif %} +138 -0
  58. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +193 -307
  59. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/app_utils/deploy.py +414 -0
  60. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/{utils → app_utils}/{% if cookiecutter.is_adk_live %}expose_app.py{% else %}unused_expose_app.py{% endif %} +13 -14
  61. agent_starter_pack/deployment_targets/cloud_run/Dockerfile +4 -1
  62. agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +85 -86
  63. agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/service.tf +139 -107
  64. agent_starter_pack/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +228 -12
  65. agent_starter_pack/deployment_targets/cloud_run/tests/load_test/README.md +4 -4
  66. agent_starter_pack/deployment_targets/cloud_run/tests/load_test/load_test.py +92 -12
  67. agent_starter_pack/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/{server.py → fast_api_app.py} +194 -121
  68. agent_starter_pack/frontends/adk_live_react/frontend/package-lock.json +18 -18
  69. agent_starter_pack/frontends/adk_live_react/frontend/src/multimodal-live-types.ts +5 -3
  70. agent_starter_pack/resources/docs/adk-cheatsheet.md +198 -41
  71. agent_starter_pack/resources/locks/uv-adk_a2a_base-agent_engine.lock +4966 -0
  72. agent_starter_pack/resources/locks/uv-adk_a2a_base-cloud_run.lock +5011 -0
  73. agent_starter_pack/resources/locks/uv-adk_base-agent_engine.lock +1443 -709
  74. agent_starter_pack/resources/locks/uv-adk_base-cloud_run.lock +1058 -874
  75. agent_starter_pack/resources/locks/uv-adk_live-agent_engine.lock +1443 -709
  76. agent_starter_pack/resources/locks/uv-adk_live-cloud_run.lock +1058 -874
  77. agent_starter_pack/resources/locks/uv-agentic_rag-agent_engine.lock +1568 -749
  78. agent_starter_pack/resources/locks/uv-agentic_rag-cloud_run.lock +1123 -929
  79. agent_starter_pack/resources/locks/{uv-langgraph_base_react-agent_engine.lock → uv-langgraph_base-agent_engine.lock} +1714 -1689
  80. agent_starter_pack/resources/locks/{uv-langgraph_base_react-cloud_run.lock → uv-langgraph_base-cloud_run.lock} +1285 -2374
  81. agent_starter_pack/utils/watch_and_rebuild.py +1 -1
  82. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/METADATA +3 -6
  83. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/RECORD +89 -93
  84. agent_starter_pack-0.21.0.dist-info/entry_points.txt +2 -0
  85. llm.txt +4 -5
  86. agent_starter_pack/agents/crewai_coding_crew/README.md +0 -34
  87. agent_starter_pack/agents/crewai_coding_crew/app/agent.py +0 -47
  88. agent_starter_pack/agents/crewai_coding_crew/app/crew/config/tasks.yaml +0 -37
  89. agent_starter_pack/agents/crewai_coding_crew/app/crew/crew.py +0 -71
  90. agent_starter_pack/agents/crewai_coding_crew/tests/integration/test_agent.py +0 -47
  91. agent_starter_pack/agents/langgraph_base_react/README.md +0 -9
  92. agent_starter_pack/agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +0 -1574
  93. agent_starter_pack/base_template/deployment/terraform/dev/log_sinks.tf +0 -69
  94. agent_starter_pack/base_template/deployment/terraform/log_sinks.tf +0 -79
  95. agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +0 -155
  96. agent_starter_pack/cli/utils/register_gemini_enterprise.py +0 -406
  97. agent_starter_pack/deployment_targets/agent_engine/deployment/terraform/{% if not cookiecutter.is_adk_live %}service.tf{% else %}unused_service.tf{% endif %} +0 -82
  98. agent_starter_pack/deployment_targets/agent_engine/notebooks/intro_agent_engine.ipynb +0 -1025
  99. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +0 -99
  100. agent_starter_pack/frontends/streamlit/frontend/side_bar.py +0 -214
  101. agent_starter_pack/frontends/streamlit/frontend/streamlit_app.py +0 -265
  102. agent_starter_pack/frontends/streamlit/frontend/utils/chat_utils.py +0 -67
  103. agent_starter_pack/frontends/streamlit/frontend/utils/local_chat_history.py +0 -127
  104. agent_starter_pack/frontends/streamlit/frontend/utils/message_editing.py +0 -59
  105. agent_starter_pack/frontends/streamlit/frontend/utils/multimodal_utils.py +0 -217
  106. agent_starter_pack/frontends/streamlit/frontend/utils/stream_handler.py +0 -310
  107. agent_starter_pack/frontends/streamlit/frontend/utils/title_summary.py +0 -94
  108. agent_starter_pack/resources/locks/uv-crewai_coding_crew-agent_engine.lock +0 -6650
  109. agent_starter_pack/resources/locks/uv-crewai_coding_crew-cloud_run.lock +0 -7825
  110. agent_starter_pack-0.18.2.dist-info/entry_points.txt +0 -3
  111. /agent_starter_pack/agents/{crewai_coding_crew → langgraph_base}/notebooks/evaluating_langgraph_agent.ipynb +0 -0
  112. /agent_starter_pack/base_template/{{cookiecutter.agent_directory}}/{utils → app_utils}/gcs.py +0 -0
  113. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/WHEEL +0 -0
  114. {agent_starter_pack-0.18.2.dist-info → agent_starter_pack-0.21.0.dist-info}/licenses/LICENSE +0 -0
@@ -47,7 +47,7 @@ def start_server() -> subprocess.Popen[str]:
47
47
  sys.executable,
48
48
  "-m",
49
49
  "uvicorn",
50
- "{{cookiecutter.agent_directory}}.server:app",
50
+ "{{cookiecutter.agent_directory}}.fast_api_app:app",
51
51
  "--host",
52
52
  "0.0.0.0",
53
53
  "--port",
@@ -202,8 +202,8 @@ def test_feedback_endpoint(server_fixture: subprocess.Popen[str]) -> None:
202
202
  feedback_data = {
203
203
  "score": 5,
204
204
  "text": "Great response!",
205
- "invocation_id": "test-run-123",
206
- "user_id": "test-user",
205
+ "user_id": "test-user-123",
206
+ "session_id": "test-session-123",
207
207
  "log_type": "feedback",
208
208
  }
209
209
 
@@ -220,12 +220,28 @@ import subprocess
220
220
  import sys
221
221
  import threading
222
222
  import time
223
+ {%- if cookiecutter.is_a2a %}
223
224
  import uuid
225
+ {%- endif %}
224
226
  from collections.abc import Iterator
225
227
  from typing import Any
226
228
 
227
229
  import pytest
228
230
  import requests
231
+ {%- if cookiecutter.is_a2a %}
232
+ from a2a.types import (
233
+ JSONRPCErrorResponse,
234
+ Message,
235
+ MessageSendParams,
236
+ Part,
237
+ Role,
238
+ SendMessageRequest,
239
+ SendMessageResponse,
240
+ SendStreamingMessageRequest,
241
+ SendStreamingMessageResponse,
242
+ TextPart,
243
+ )
244
+ {%- endif %}
229
245
  from requests.exceptions import RequestException
230
246
 
231
247
  # Configure logging
@@ -233,7 +249,10 @@ logging.basicConfig(level=logging.INFO)
233
249
  logger = logging.getLogger(__name__)
234
250
 
235
251
  BASE_URL = "http://127.0.0.1:8000/"
236
- {%- if cookiecutter.is_adk %}
252
+ {%- if cookiecutter.is_a2a %}
253
+ A2A_RPC_URL = BASE_URL + "a2a/{{cookiecutter.agent_directory}}/"
254
+ AGENT_CARD_URL = A2A_RPC_URL + ".well-known/agent-card.json"
255
+ {%- elif cookiecutter.is_adk %}
237
256
  STREAM_URL = BASE_URL + "run_sse"
238
257
  {%- else %}
239
258
  STREAM_URL = BASE_URL + "stream_messages"
@@ -255,7 +274,7 @@ def start_server() -> subprocess.Popen[str]:
255
274
  sys.executable,
256
275
  "-m",
257
276
  "uvicorn",
258
- "{{cookiecutter.agent_directory}}.server:app",
277
+ "{{cookiecutter.agent_directory}}.fast_api_app:app",
259
278
  "--host",
260
279
  "0.0.0.0",
261
280
  "--port",
@@ -264,8 +283,8 @@ def start_server() -> subprocess.Popen[str]:
264
283
  env = os.environ.copy()
265
284
  env["INTEGRATION_TEST"] = "TRUE"
266
285
  {%- if cookiecutter.session_type == "agent_engine" %}
267
- # Set test-specific agent engine session name
268
- env["AGENT_ENGINE_SESSION_NAME"] = "test-{{cookiecutter.project_name}}"
286
+ # Use in-memory session for local E2E tests instead of creating Agent Engine
287
+ env["USE_IN_MEMORY_SESSION"] = "true"
269
288
  {%- endif %}
270
289
  process = subprocess.Popen(
271
290
  command,
@@ -292,7 +311,11 @@ def wait_for_server(timeout: int = 90, interval: int = 1) -> bool:
292
311
  start_time = time.time()
293
312
  while time.time() - start_time < timeout:
294
313
  try:
314
+ {%- if cookiecutter.is_a2a %}
315
+ response = requests.get(AGENT_CARD_URL, timeout=10)
316
+ {%- else %}
295
317
  response = requests.get("http://127.0.0.1:8000/docs", timeout=10)
318
+ {%- endif %}
296
319
  if response.status_code == 200:
297
320
  logger.info("Server is ready")
298
321
  return True
@@ -323,6 +346,82 @@ def server_fixture(request: Any) -> Iterator[subprocess.Popen[str]]:
323
346
 
324
347
 
325
348
  def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
349
+ {%- if cookiecutter.is_a2a %}
350
+ """Test the chat stream functionality using A2A JSON-RPC protocol."""
351
+ logger.info("Starting chat stream test")
352
+
353
+ message = Message(
354
+ message_id=f"msg-user-{uuid.uuid4()}",
355
+ role=Role.user,
356
+ parts=[Part(root=TextPart(text="What's the weather in San Francisco?"))],
357
+ )
358
+
359
+ request = SendStreamingMessageRequest(
360
+ id="test-req-001",
361
+ params=MessageSendParams(message=message),
362
+ )
363
+
364
+ # Send the request
365
+ response = requests.post(
366
+ A2A_RPC_URL,
367
+ headers=HEADERS,
368
+ json=request.model_dump(mode="json", exclude_none=True),
369
+ stream=True,
370
+ timeout=60,
371
+ )
372
+ assert response.status_code == 200
373
+
374
+ # Parse streaming JSON-RPC responses
375
+ responses: list[SendStreamingMessageResponse] = []
376
+
377
+ for line in response.iter_lines():
378
+ if line:
379
+ line_str = line.decode("utf-8")
380
+ if line_str.startswith("data: "):
381
+ event_json = line_str[6:]
382
+ json_data = json.loads(event_json)
383
+ streaming_response = SendStreamingMessageResponse.model_validate(
384
+ json_data
385
+ )
386
+ responses.append(streaming_response)
387
+
388
+ assert responses, "No responses received from stream"
389
+
390
+ # Check for final status update
391
+ final_responses = [
392
+ r.root
393
+ for r in responses
394
+ if hasattr(r.root, "result")
395
+ and hasattr(r.root.result, "final")
396
+ and r.root.result.final is True
397
+ ]
398
+ assert final_responses, "No final response received"
399
+
400
+ final_response = final_responses[-1]
401
+ assert final_response.result.kind == "status-update"
402
+ assert hasattr(final_response.result, "status")
403
+ assert final_response.result.status.state == "completed"
404
+
405
+ # Check for artifact content
406
+ artifact_responses = [
407
+ r.root
408
+ for r in responses
409
+ if hasattr(r.root, "result") and r.root.result.kind == "artifact-update"
410
+ ]
411
+ assert artifact_responses, "No artifact content received in stream"
412
+
413
+ # Verify text content is in the artifact
414
+ artifact_response = artifact_responses[-1]
415
+ assert hasattr(artifact_response.result, "artifact")
416
+ artifact = artifact_response.result.artifact
417
+ assert artifact.parts, "Artifact has no parts"
418
+
419
+ has_text = any(
420
+ part.root.kind == "text" and hasattr(part.root, "text") and part.root.text
421
+ for part in artifact.parts
422
+ )
423
+ assert has_text, "No text content found in artifact"
424
+ {%- else %}
326
425
  """Test the chat stream functionality."""
327
426
  logger.info("Starting chat stream test")
328
427
  {% if cookiecutter.is_adk %}
@@ -417,6 +516,94 @@ def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
417
516
  break
418
517
  assert has_content, "At least one message should have content"
419
518
  {%- endif %}
519
+ {%- endif %}
520
+
521
+
522
+ {%- if cookiecutter.is_a2a %}
523
+
524
+
525
+ def test_chat_non_streaming(server_fixture: subprocess.Popen[str]) -> None:
526
+ """Test the non-streaming chat functionality using A2A JSON-RPC protocol."""
527
+ logger.info("Starting non-streaming chat test")
528
+
529
+ message = Message(
530
+ message_id=f"msg-user-{uuid.uuid4()}",
531
+ role=Role.user,
532
+ parts=[Part(root=TextPart(text="What's the weather in San Francisco?"))],
533
+ )
534
+
535
+ request = SendMessageRequest(
536
+ id="test-req-002",
537
+ params=MessageSendParams(message=message),
538
+ )
539
+
540
+ response = requests.post(
541
+ A2A_RPC_URL,
542
+ headers=HEADERS,
543
+ json=request.model_dump(mode="json", exclude_none=True),
544
+ timeout=60,
545
+ )
546
+ assert response.status_code == 200
547
+
548
+ # Parse the single JSON-RPC response
549
+ response_data = response.json()
550
+ message_response = SendMessageResponse.model_validate(response_data)
551
+ logger.info(f"Received response: {message_response}")
552
+
553
+ # For non-streaming, the result is a Task object
554
+ json_rpc_resp = message_response.root
555
+ assert hasattr(json_rpc_resp, "result")
556
+ task = json_rpc_resp.result
557
+ assert task.kind == "task"
558
+ assert hasattr(task, "status")
559
+ assert task.status.state == "completed"
560
+
561
+ # Check that we got artifacts (the final agent output)
562
+ assert hasattr(task, "artifacts")
563
+ assert task.artifacts, "No artifacts in task"
564
+
565
+ # Verify we got text content in the artifact
566
+ artifact = task.artifacts[0]
567
+ assert artifact.parts, "Artifact has no parts"
568
+
569
+ has_text = any(
570
+ part.root.kind == "text" and hasattr(part.root, "text") and part.root.text
571
+ for part in artifact.parts
572
+ )
573
+ assert has_text, "No text content found in artifact"
574
+
575
+
576
+ def test_chat_stream_error_handling(server_fixture: subprocess.Popen[str]) -> None:
577
+ """Test the chat stream error handling with invalid A2A request."""
578
+ logger.info("Starting chat stream error handling test")
579
+
580
+ invalid_data = {
581
+ "jsonrpc": "2.0",
582
+ "id": "test-error-001",
583
+ "method": "message/send",
584
+ "params": {
585
+ "message": {
586
+ "role": "user",
587
+ # Missing required 'parts' field
588
+ "messageId": f"msg-user-{uuid.uuid4()}",
589
+ }
590
+ },
591
+ }
592
+
593
+ response = requests.post(
594
+ A2A_RPC_URL, headers=HEADERS, json=invalid_data, timeout=10
595
+ )
596
+ assert response.status_code == 200
597
+
598
+ response_data = response.json()
599
+ error_response = JSONRPCErrorResponse.model_validate(response_data)
600
+ assert "error" in response_data, "Expected JSON-RPC error in response"
601
+
602
+ # Assert error for invalid parameters
603
+ assert error_response.error.code == -32602
604
+
605
+ logger.info("Error handling test completed successfully")
606
+ {%- else %}
420
607
 
421
608
 
422
609
  def test_chat_stream_error_handling(server_fixture: subprocess.Popen[str]) -> None:
@@ -433,6 +620,7 @@ def test_chat_stream_error_handling(server_fixture: subprocess.Popen[str]) -> No
433
620
  f"Expected status code 422, got {response.status_code}"
434
621
  )
435
622
  logger.info("Error handling test completed successfully")
623
+ {%- endif %}
436
624
 
437
625
 
438
626
  def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
@@ -443,11 +631,8 @@ def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
443
631
  # Create sample feedback data
444
632
  feedback_data = {
445
633
  "score": 4,
446
- {%- if cookiecutter.is_adk %}
447
- "invocation_id": str(uuid.uuid4()),
448
- {%- else %}
449
- "run_id": str(uuid.uuid4()),
450
- {%- endif %}
634
+ "user_id": "test-user-456",
635
+ "session_id": "test-session-456",
451
636
  "text": "Great response!",
452
637
  }
453
638
 
@@ -455,6 +640,37 @@ def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
455
640
  FEEDBACK_URL, json=feedback_data, headers=HEADERS, timeout=10
456
641
  )
457
642
  assert response.status_code == 200
643
+
644
+
645
+ {%- if cookiecutter.is_a2a %}
646
+
647
+
648
+ def test_a2a_agent_json_generation(server_fixture: subprocess.Popen[str]) -> None:
649
+ """
650
+ Test that the agent.json file is automatically generated and served correctly
651
+ via the well-known URI.
652
+ """
653
+ # Verify the A2A endpoint serves the agent card
654
+ response = requests.get(AGENT_CARD_URL, timeout=10)
655
+ assert response.status_code == 200, f"A2A endpoint returned {response.status_code}"
656
+
657
+ # Validate required fields in served agent card
658
+ served_agent_card = response.json()
659
+ required_fields = [
660
+ "name",
661
+ "description",
662
+ "skills",
663
+ "capabilities",
664
+ "url",
665
+ "version",
666
+ ]
667
+ for field in required_fields:
668
+ assert field in served_agent_card, (
669
+ f"Missing required field in served agent card: {field}"
670
+ )
671
+
672
+
673
+ {%- endif %}
458
674
  {%- if cookiecutter.session_type == "agent_engine" %}
459
675
 
460
676
 
@@ -12,7 +12,7 @@ Follow these steps to execute load tests on your local machine:
12
12
  Launch the FastAPI server in a separate terminal:
13
13
 
14
14
  ```bash
15
- uv run uvicorn {{cookiecutter.agent_directory}}.server:app --host 0.0.0.0 --port 8000 --reload
15
+ uv run uvicorn {{cookiecutter.agent_directory}}.fast_api_app:app --host 0.0.0.0 --port 8000 --reload
16
16
  ```
17
17
 
18
18
  **2. (In another tab) Create virtual environment with Locust**
@@ -92,14 +92,14 @@ Follow these steps to execute load tests on your local machine:
92
92
  Launch the FastAPI server in a separate terminal:
93
93
 
94
94
  ```bash
95
- uv run uvicorn {{cookiecutter.agent_directory}}.server:app --host 0.0.0.0 --port 8000 --reload
95
+ uv run uvicorn {{cookiecutter.agent_directory}}.fast_api_app:app --host 0.0.0.0 --port 8000 --reload
96
96
  ```
97
97
 
98
98
  **2. (In another tab) Create virtual environment with Locust**
99
99
  Using another terminal tab, This is suggested to avoid conflicts with the existing application python environment.
100
100
 
101
101
  ```bash
102
- python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1
102
+ python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1{%- if cookiecutter.is_a2a %} a2a-sdk~=0.3.9{%- endif %}
103
103
  ```
104
104
 
105
105
  **3. Execute the Load Test:**
@@ -150,7 +150,7 @@ export _ID_TOKEN=$(gcloud auth print-identity-token -q)
150
150
  **3. Execute the Load Test:**
151
151
  Create virtual environment with Locust:
152
152
  ```bash
153
- python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1
153
+ python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1{%- if cookiecutter.is_a2a %} a2a-sdk~=0.3.9{%- endif %}
154
154
  ```
155
155
 
156
156
  Execute load tests. The following command executes the same load test parameters as the local test but targets your remote Cloud Run instance.
@@ -138,9 +138,23 @@ class RemoteAgentUser(WebSocketUser):
138
138
  host = "http://localhost:8000" # Default for local testing
139
139
  {%- else %}
140
140
 
141
+ import json
142
+ import logging
141
143
  import os
142
144
  import time
143
- {%- if cookiecutter.is_adk %}
145
+ {%- if cookiecutter.is_a2a %}
146
+ import uuid
147
+
148
+ from a2a.types import (
149
+ Message,
150
+ MessageSendParams,
151
+ Part,
152
+ Role,
153
+ SendStreamingMessageRequest,
154
+ TextPart,
155
+ )
156
+ from locust import HttpUser, between, task
157
+ {%- elif cookiecutter.is_adk %}
144
158
  import uuid
145
159
 
146
160
  import requests
@@ -149,11 +163,23 @@ from locust import HttpUser, between, task
149
163
 
150
164
  from locust import HttpUser, between, task
151
165
  {%- endif %}
152
- {% if cookiecutter.is_adk %}
166
+ {%- if cookiecutter.is_a2a %}
167
+
168
+ ENDPOINT = "/a2a/{{cookiecutter.agent_directory}}"
169
+ {%- elif cookiecutter.is_adk %}
170
+
153
171
  ENDPOINT = "/run_sse"
154
- {% else %}
172
+ {%- else %}
173
+
155
174
  ENDPOINT = "/stream_messages"
156
- {% endif %}
175
+ {%- endif %}
176
+
177
+ # Configure logging
178
+ logging.basicConfig(
179
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
180
+ )
181
+ logger = logging.getLogger(__name__)
182
+
157
183
 
158
184
  class ChatStreamUser(HttpUser):
159
185
  """Simulates a user interacting with the chat stream API."""
@@ -162,6 +188,34 @@ class ChatStreamUser(HttpUser):
162
188
 
163
189
  @task
164
190
  def chat_stream(self) -> None:
191
+ {%- if cookiecutter.is_a2a %}
192
+ """Simulates a chat stream interaction using A2A protocol."""
193
+ headers = {"Content-Type": "application/json"}
194
+ if os.environ.get("_ID_TOKEN"):
195
+ headers["Authorization"] = f"Bearer {os.environ['_ID_TOKEN']}"
196
+
197
+ message = Message(
198
+ message_id=f"msg-user-{uuid.uuid4()}",
199
+ role=Role.user,
200
+ parts=[Part(root=TextPart(text="Hello! What's the weather in New York?"))],
201
+ )
202
+
203
+ request = SendStreamingMessageRequest(
204
+ id=f"req-{uuid.uuid4()}",
205
+ params=MessageSendParams(message=message),
206
+ )
207
+
208
+ start_time = time.time()
209
+
210
+ with self.client.post(
211
+ ENDPOINT,
212
+ name=f"{ENDPOINT} message",
213
+ headers=headers,
214
+ json=request.model_dump(mode="json", exclude_none=True),
215
+ catch_response=True,
216
+ stream=True,
217
+ ) as response:
218
+ {%- else %}
165
219
  """Simulates a chat stream interaction."""
166
220
  headers = {"Content-Type": "application/json"}
167
221
  if os.environ.get("_ID_TOKEN"):
@@ -218,8 +272,10 @@ class ChatStreamUser(HttpUser):
218
272
  stream=True,
219
273
  params={"alt": "sse"},
220
274
  ) as response:
275
+ {%- endif %}
221
276
  if response.status_code == 200:
222
277
  events = []
278
+ has_error = False
223
279
  for line in response.iter_lines():
224
280
  if line:
225
281
  line_str = line.decode("utf-8")
@@ -234,16 +290,40 @@ class ChatStreamUser(HttpUser):
234
290
  response=response,
235
291
  context={},
236
292
  )
293
+
294
+ # Check for error responses in the JSON payload
295
+ try:
296
+ event_data = json.loads(line_str)
297
+ if isinstance(event_data, dict) and "code" in event_data:
298
+ # Flag any non-2xx codes as errors
299
+ if event_data["code"] >= 400:
300
+ has_error = True
301
+ error_msg = event_data.get(
302
+ "message", "Unknown error"
303
+ )
304
+ response.failure(f"Error in response: {error_msg}")
305
+ logger.error(
306
+ "Received error response: code=%s, message=%s",
307
+ event_data["code"],
308
+ error_msg,
309
+ )
310
+ except json.JSONDecodeError:
311
+ # If it's not valid JSON, continue processing
312
+ pass
313
+
237
314
  end_time = time.time()
238
315
  total_time = end_time - start_time
239
- self.environment.events.request.fire(
240
- request_type="POST",
241
- name=f"{ENDPOINT} end",
242
- response_time=total_time * 1000, # Convert to milliseconds
243
- response_length=len(events),
244
- response=response,
245
- context={},
246
- )
316
+
317
+ # Only fire success event if no errors were found
318
+ if not has_error:
319
+ self.environment.events.request.fire(
320
+ request_type="POST",
321
+ name=f"{ENDPOINT} end",
322
+ response_time=total_time * 1000, # Convert to milliseconds
323
+ response_length=len(events),
324
+ response=response,
325
+ context={},
326
+ )
247
327
  else:
248
328
  response.failure(f"Unexpected status code: {response.status_code}")
249
329
  {%- endif %}