agent-starter-pack 0.15.7__py3-none-any.whl → 0.17.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.

Files changed (195) hide show
  1. {agents → agent_starter_pack/agents}/adk_base/.template/templateconfig.yaml +1 -1
  2. {agents/live_api → agent_starter_pack/agents/adk_live}/.template/templateconfig.yaml +5 -7
  3. agent_starter_pack/agents/adk_live/README.md +32 -0
  4. agent_starter_pack/agents/adk_live/app/agent.py +48 -0
  5. agent_starter_pack/agents/adk_live/tests/unit/test_dummy.py +38 -0
  6. {agents → agent_starter_pack/agents}/agentic_rag/.template/templateconfig.yaml +1 -1
  7. agent_starter_pack/agents/crewai_coding_crew/app/agent.py +47 -0
  8. agent_starter_pack/agents/langgraph_base_react/app/agent.py +34 -0
  9. {src → agent_starter_pack}/base_template/GEMINI.md +1 -1
  10. {src → agent_starter_pack}/base_template/Makefile +130 -61
  11. {src → agent_starter_pack}/base_template/README.md +6 -6
  12. {src → agent_starter_pack}/base_template/deployment/terraform/dev/apis.tf +1 -1
  13. {src → agent_starter_pack}/base_template/deployment/terraform/dev/log_sinks.tf +31 -25
  14. {src → agent_starter_pack}/base_template/deployment/terraform/dev/providers.tf +1 -1
  15. {src → agent_starter_pack}/base_template/deployment/terraform/dev/variables.tf +1 -1
  16. {src → agent_starter_pack}/base_template/deployment/terraform/github.tf +14 -0
  17. {src → agent_starter_pack}/base_template/deployment/terraform/locals.tf +1 -1
  18. {src → agent_starter_pack}/base_template/deployment/terraform/log_sinks.tf +37 -28
  19. {src → agent_starter_pack}/base_template/deployment/terraform/providers.tf +1 -1
  20. {src → agent_starter_pack}/base_template/deployment/terraform/variables.tf +1 -1
  21. {src → agent_starter_pack}/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}build_triggers.tf{% else %}unused_build_triggers.tf{% endif %} +4 -2
  22. {src → agent_starter_pack}/base_template/pyproject.toml +22 -21
  23. {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +5 -5
  24. {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +3 -3
  25. {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +74 -11
  26. {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +6 -6
  27. {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/pr_checks.yaml +4 -4
  28. {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +95 -13
  29. {src → agent_starter_pack}/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +1 -1
  30. {src → agent_starter_pack}/base_template/{{cookiecutter.agent_directory}}/utils/typing.py +4 -4
  31. {src → agent_starter_pack}/cli/commands/setup_cicd.py +1 -1
  32. {src → agent_starter_pack}/cli/main.py +2 -2
  33. {src → agent_starter_pack}/cli/utils/gcp.py +1 -1
  34. {src → agent_starter_pack}/cli/utils/remote_template.py +12 -9
  35. {src → agent_starter_pack}/cli/utils/template.py +19 -15
  36. agent_starter_pack/deployment_targets/agent_engine/deployment/terraform/{% if not cookiecutter.is_adk_live %}service.tf{% else %}unused_service.tf{% endif %} +82 -0
  37. agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +387 -0
  38. agent_starter_pack/deployment_targets/agent_engine/tests/load_test/README.md +84 -0
  39. agent_starter_pack/deployment_targets/agent_engine/tests/load_test/load_test.py +255 -0
  40. {src → agent_starter_pack}/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +40 -14
  41. {src → agent_starter_pack}/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +13 -4
  42. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/{% if cookiecutter.is_adk_live %}expose_app.py{% else %}unused_expose_app.py{% endif %} +520 -0
  43. {src → agent_starter_pack}/deployment_targets/cloud_run/Dockerfile +3 -3
  44. {src → agent_starter_pack}/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +4 -4
  45. {src → agent_starter_pack}/deployment_targets/cloud_run/deployment/terraform/service.tf +7 -7
  46. {src → agent_starter_pack}/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +207 -5
  47. {src → agent_starter_pack}/deployment_targets/cloud_run/tests/load_test/README.md +82 -0
  48. agent_starter_pack/deployment_targets/cloud_run/tests/load_test/load_test.py +249 -0
  49. {src → agent_starter_pack}/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +190 -146
  50. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/package-lock.json +39 -1007
  51. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/package.json +1 -9
  52. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/App.tsx +1 -1
  53. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/logger/Logger.tsx +8 -3
  54. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/logger/logger.scss +26 -0
  55. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/side-panel/SidePanel.tsx +11 -5
  56. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/side-panel/side-panel.scss +146 -115
  57. agent_starter_pack/frontends/adk_live_react/frontend/src/components/transcription-preview/TranscriptionPreview.tsx +106 -0
  58. agent_starter_pack/frontends/adk_live_react/frontend/src/components/transcription-preview/transcription-preview.scss +150 -0
  59. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-live-api.ts +8 -2
  60. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/multimodal-live-types.ts +38 -2
  61. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/audio-recorder.ts +1 -1
  62. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/audio-streamer.ts +1 -1
  63. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/multimodal-live-client.ts +204 -23
  64. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/utils.ts +27 -5
  65. {src → agent_starter_pack}/frontends/streamlit/frontend/utils/local_chat_history.py +2 -0
  66. {src → agent_starter_pack}/resources/docs/adk-cheatsheet.md +5 -5
  67. agent_starter_pack/resources/idx/.idx/dev.nix +64 -0
  68. agent_starter_pack/resources/idx/idx-template.json +6 -0
  69. {src → agent_starter_pack}/resources/idx/idx-template.nix +2 -3
  70. {src → agent_starter_pack}/resources/locks/uv-adk_base-agent_engine.lock +1079 -954
  71. {src → agent_starter_pack}/resources/locks/uv-adk_base-cloud_run.lock +1441 -1309
  72. agent_starter_pack/resources/locks/uv-adk_live-agent_engine.lock +4229 -0
  73. agent_starter_pack/resources/locks/uv-adk_live-cloud_run.lock +4822 -0
  74. {src → agent_starter_pack}/resources/locks/uv-agentic_rag-agent_engine.lock +1107 -997
  75. {src → agent_starter_pack}/resources/locks/uv-agentic_rag-cloud_run.lock +1485 -1368
  76. {src → agent_starter_pack}/resources/locks/uv-crewai_coding_crew-agent_engine.lock +1294 -1297
  77. {src → agent_starter_pack}/resources/locks/uv-crewai_coding_crew-cloud_run.lock +2028 -1807
  78. {src → agent_starter_pack}/resources/locks/uv-langgraph_base_react-agent_engine.lock +1176 -1197
  79. {src → agent_starter_pack}/resources/locks/uv-langgraph_base_react-cloud_run.lock +1947 -1679
  80. {src → agent_starter_pack}/utils/generate_locks.py +12 -7
  81. {src → agent_starter_pack}/utils/lock_utils.py +2 -2
  82. {src → agent_starter_pack}/utils/watch_and_rebuild.py +1 -1
  83. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.17.0.dist-info}/METADATA +17 -18
  84. agent_starter_pack-0.17.0.dist-info/RECORD +179 -0
  85. agent_starter_pack-0.17.0.dist-info/entry_points.txt +2 -0
  86. llm.txt +1 -1
  87. agent_starter_pack-0.15.7.dist-info/RECORD +0 -176
  88. agent_starter_pack-0.15.7.dist-info/entry_points.txt +0 -2
  89. agents/crewai_coding_crew/app/agent.py +0 -86
  90. agents/langgraph_base_react/app/agent.py +0 -73
  91. agents/live_api/README.md +0 -37
  92. agents/live_api/app/agent.py +0 -72
  93. agents/live_api/tests/integration/test_server_e2e.py +0 -260
  94. agents/live_api/tests/load_test/load_test.py +0 -40
  95. agents/live_api/tests/unit/test_server.py +0 -144
  96. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +0 -186
  97. src/deployment_targets/agent_engine/tests/load_test/README.md +0 -37
  98. src/deployment_targets/agent_engine/tests/load_test/load_test.py +0 -126
  99. src/deployment_targets/cloud_run/tests/load_test/load_test.py +0 -122
  100. src/resources/idx/.idx/dev.nix +0 -50
  101. src/resources/idx/idx-template.json +0 -21
  102. src/resources/locks/uv-live_api-cloud_run.lock +0 -6118
  103. {agents → agent_starter_pack/agents}/README.md +0 -0
  104. {agents → agent_starter_pack/agents}/adk_base/README.md +0 -0
  105. {agents → agent_starter_pack/agents}/adk_base/app/__init__.py +0 -0
  106. {agents → agent_starter_pack/agents}/adk_base/app/agent.py +0 -0
  107. {agents → agent_starter_pack/agents}/adk_base/notebooks/adk_app_testing.ipynb +0 -0
  108. {agents → agent_starter_pack/agents}/adk_base/notebooks/evaluating_adk_agent.ipynb +0 -0
  109. {agents → agent_starter_pack/agents}/adk_base/tests/integration/test_agent.py +0 -0
  110. {agents → agent_starter_pack/agents}/agentic_rag/README.md +0 -0
  111. {agents → agent_starter_pack/agents}/agentic_rag/app/__init__.py +0 -0
  112. {agents → agent_starter_pack/agents}/agentic_rag/app/agent.py +0 -0
  113. {agents → agent_starter_pack/agents}/agentic_rag/app/retrievers.py +0 -0
  114. {agents → agent_starter_pack/agents}/agentic_rag/app/templates.py +0 -0
  115. {agents → agent_starter_pack/agents}/agentic_rag/notebooks/adk_app_testing.ipynb +0 -0
  116. {agents → agent_starter_pack/agents}/agentic_rag/notebooks/evaluating_adk_agent.ipynb +0 -0
  117. {agents → agent_starter_pack/agents}/agentic_rag/tests/integration/test_agent.py +0 -0
  118. {agents → agent_starter_pack/agents}/crewai_coding_crew/.template/templateconfig.yaml +0 -0
  119. {agents → agent_starter_pack/agents}/crewai_coding_crew/README.md +0 -0
  120. {agents → agent_starter_pack/agents}/crewai_coding_crew/app/crew/config/agents.yaml +0 -0
  121. {agents → agent_starter_pack/agents}/crewai_coding_crew/app/crew/config/tasks.yaml +0 -0
  122. {agents → agent_starter_pack/agents}/crewai_coding_crew/app/crew/crew.py +0 -0
  123. {agents → agent_starter_pack/agents}/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +0 -0
  124. {agents → agent_starter_pack/agents}/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +0 -0
  125. {agents → agent_starter_pack/agents}/crewai_coding_crew/tests/integration/test_agent.py +0 -0
  126. {agents → agent_starter_pack/agents}/langgraph_base_react/.template/templateconfig.yaml +0 -0
  127. {agents → agent_starter_pack/agents}/langgraph_base_react/README.md +0 -0
  128. {agents → agent_starter_pack/agents}/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +0 -0
  129. {agents → agent_starter_pack/agents}/langgraph_base_react/tests/integration/test_agent.py +0 -0
  130. {src → agent_starter_pack}/base_template/.gitignore +0 -0
  131. {src → agent_starter_pack}/base_template/deployment/README.md +0 -0
  132. {src → agent_starter_pack}/base_template/deployment/terraform/apis.tf +0 -0
  133. {src → agent_starter_pack}/base_template/deployment/terraform/dev/iam.tf +0 -0
  134. {src → agent_starter_pack}/base_template/deployment/terraform/dev/storage.tf +0 -0
  135. {src → agent_starter_pack}/base_template/deployment/terraform/dev/vars/env.tfvars +0 -0
  136. {src → agent_starter_pack}/base_template/deployment/terraform/iam.tf +0 -0
  137. {src → agent_starter_pack}/base_template/deployment/terraform/service_accounts.tf +0 -0
  138. {src → agent_starter_pack}/base_template/deployment/terraform/storage.tf +0 -0
  139. {src → agent_starter_pack}/base_template/deployment/terraform/vars/env.tfvars +0 -0
  140. {src → agent_starter_pack}/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'github_actions' %}wif.tf{% else %}unused_wif.tf{% endif %} +0 -0
  141. {src → agent_starter_pack}/base_template/tests/unit/test_dummy.py +0 -0
  142. {src → agent_starter_pack}/base_template/{{cookiecutter.agent_directory}}/utils/gcs.py +0 -0
  143. {src → agent_starter_pack}/cli/commands/create.py +0 -0
  144. {src → agent_starter_pack}/cli/commands/enhance.py +0 -0
  145. {src → agent_starter_pack}/cli/commands/list.py +0 -0
  146. {src → agent_starter_pack}/cli/utils/__init__.py +0 -0
  147. {src → agent_starter_pack}/cli/utils/cicd.py +0 -0
  148. {src → agent_starter_pack}/cli/utils/datastores.py +0 -0
  149. {src → agent_starter_pack}/cli/utils/logging.py +0 -0
  150. {src → agent_starter_pack}/cli/utils/version.py +0 -0
  151. {src → agent_starter_pack}/data_ingestion/README.md +0 -0
  152. {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +0 -0
  153. {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/components/process_data.py +0 -0
  154. {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/pipeline.py +0 -0
  155. {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +0 -0
  156. {src → agent_starter_pack}/data_ingestion/pyproject.toml +0 -0
  157. {src → agent_starter_pack}/data_ingestion/uv.lock +0 -0
  158. {src → agent_starter_pack}/deployment_targets/agent_engine/deployment_metadata.json +0 -0
  159. {src → agent_starter_pack}/deployment_targets/agent_engine/notebooks/intro_agent_engine.ipynb +0 -0
  160. {src → agent_starter_pack}/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
  161. {src → agent_starter_pack}/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
  162. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/public/favicon.ico +0 -0
  163. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/public/index.html +0 -0
  164. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/public/robots.txt +0 -0
  165. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/App.scss +0 -0
  166. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/App.test.tsx +0 -0
  167. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/audio-pulse/AudioPulse.tsx +0 -0
  168. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/audio-pulse/audio-pulse.scss +0 -0
  169. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/logger/mock-logs.ts +0 -0
  170. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/contexts/LiveAPIContext.tsx +0 -0
  171. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-media-stream-mux.ts +0 -0
  172. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-screen-capture.ts +0 -0
  173. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-webcam.ts +0 -0
  174. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/index.css +0 -0
  175. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/index.tsx +0 -0
  176. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/react-app-env.d.ts +0 -0
  177. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/reportWebVitals.ts +0 -0
  178. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/setupTests.ts +0 -0
  179. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/audioworklet-registry.ts +0 -0
  180. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/store-logger.ts +0 -0
  181. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/worklets/audio-processing.ts +0 -0
  182. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/worklets/vol-meter.ts +0 -0
  183. {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/tsconfig.json +0 -0
  184. {src → agent_starter_pack}/frontends/streamlit/frontend/side_bar.py +0 -0
  185. {src → agent_starter_pack}/frontends/streamlit/frontend/streamlit_app.py +0 -0
  186. {src → agent_starter_pack}/frontends/streamlit/frontend/style/app_markdown.py +0 -0
  187. {src → agent_starter_pack}/frontends/streamlit/frontend/utils/chat_utils.py +0 -0
  188. {src → agent_starter_pack}/frontends/streamlit/frontend/utils/message_editing.py +0 -0
  189. {src → agent_starter_pack}/frontends/streamlit/frontend/utils/multimodal_utils.py +0 -0
  190. {src → agent_starter_pack}/frontends/streamlit/frontend/utils/stream_handler.py +0 -0
  191. {src → agent_starter_pack}/frontends/streamlit/frontend/utils/title_summary.py +0 -0
  192. {src → agent_starter_pack}/resources/containers/data_processing/Dockerfile +0 -0
  193. {src → agent_starter_pack}/resources/containers/e2e-tests/Dockerfile +0 -0
  194. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.17.0.dist-info}/WHEEL +0 -0
  195. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.17.0.dist-info}/licenses/LICENSE +0 -0
@@ -11,6 +11,207 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ {%- if cookiecutter.agent_name == "adk_live" %}
15
+
16
+ import asyncio
17
+ import json
18
+ import logging
19
+ import subprocess
20
+ import sys
21
+ import threading
22
+ import time
23
+ from collections.abc import Iterator
24
+ from typing import Any
25
+
26
+ import pytest
27
+ import requests
28
+ from websockets.asyncio.client import connect
29
+
30
+ # Configure logging
31
+ logging.basicConfig(level=logging.DEBUG)
32
+ logger = logging.getLogger(__name__)
33
+
34
+ WS_URL = "ws://127.0.0.1:8000/ws"
35
+ FEEDBACK_URL = "http://127.0.0.1:8000/feedback"
36
+
37
+
38
+ def log_output(pipe: Any, log_func: Any) -> None:
39
+ """Log the output from the given pipe."""
40
+ for line in iter(pipe.readline, ""):
41
+ log_func(line.strip())
42
+
43
+
44
+ def start_server() -> subprocess.Popen[str]:
45
+ """Start the server in local mode."""
46
+ command = [
47
+ sys.executable,
48
+ "-m",
49
+ "uvicorn",
50
+ "{{cookiecutter.agent_directory}}.server:app",
51
+ "--host",
52
+ "0.0.0.0",
53
+ "--port",
54
+ "8000",
55
+ ]
56
+ process = subprocess.Popen(
57
+ command,
58
+ stdout=subprocess.PIPE,
59
+ stderr=subprocess.PIPE,
60
+ text=True,
61
+ bufsize=1,
62
+ encoding="utf-8",
63
+ )
64
+
65
+ # Start threads to log stdout and stderr in real-time
66
+ threading.Thread(
67
+ target=log_output, args=(process.stdout, logger.info), daemon=True
68
+ ).start()
69
+ threading.Thread(
70
+ target=log_output, args=(process.stderr, logger.error), daemon=True
71
+ ).start()
72
+
73
+ return process
74
+
75
+
76
+ def wait_for_server(timeout: int = 60, interval: int = 1) -> bool:
77
+ """Wait for the server to be ready."""
78
+ start_time = time.time()
79
+ while time.time() - start_time < timeout:
80
+ try:
81
+ response = requests.get("http://127.0.0.1:8000/docs", timeout=10)
82
+ if response.status_code == 200:
83
+ logger.info("Server is ready")
84
+ return True
85
+ except Exception:
86
+ pass
87
+ time.sleep(interval)
88
+ logger.error(f"Server did not become ready within {timeout} seconds")
89
+ return False
90
+
91
+
92
+ @pytest.fixture(scope="module")
93
+ def server_fixture(request: Any) -> Iterator[subprocess.Popen[str]]:
94
+ """Pytest fixture to start and stop the server for testing."""
95
+ logger.info("Starting server process")
96
+ server_process = start_server()
97
+ if not wait_for_server():
98
+ pytest.fail("Server failed to start")
99
+ logger.info("Server process started")
100
+
101
+ def stop_server() -> None:
102
+ logger.info("Stopping server process")
103
+ server_process.terminate()
104
+ try:
105
+ server_process.wait(timeout=5)
106
+ except subprocess.TimeoutExpired:
107
+ logger.warning("Server process did not terminate, killing it")
108
+ server_process.kill()
109
+ server_process.wait()
110
+ logger.info("Server process stopped")
111
+
112
+ request.addfinalizer(stop_server)
113
+ yield server_process
114
+
115
+
116
+ @pytest.mark.asyncio
117
+ async def test_websocket_audio_input(server_fixture: subprocess.Popen[str]) -> None:
118
+ """Test websocket with audio input in local mode."""
119
+
120
+ async def send_message(websocket: Any, message: dict[str, Any]) -> None:
121
+ """Helper to send JSON messages."""
122
+ await websocket.send(json.dumps(message))
123
+
124
+ async def receive_message(websocket: Any, timeout: float = 5.0) -> dict[str, Any]:
125
+ """Helper to receive messages with timeout."""
126
+ try:
127
+ response = await asyncio.wait_for(websocket.recv(), timeout=timeout)
128
+ if isinstance(response, bytes):
129
+ return json.loads(response.decode())
130
+ if isinstance(response, str):
131
+ return json.loads(response)
132
+ return response
133
+ except asyncio.TimeoutError as exc:
134
+ raise TimeoutError(
135
+ f"No response received within {timeout} seconds"
136
+ ) from exc
137
+
138
+ try:
139
+ await asyncio.sleep(2)
140
+
141
+ async with connect(WS_URL, ping_timeout=10, close_timeout=10) as websocket:
142
+ try:
143
+ # Wait for setupComplete
144
+ setup_response = await receive_message(websocket, timeout=10.0)
145
+ assert "setupComplete" in setup_response
146
+ logger.info("Received setupComplete")
147
+
148
+ # Send dummy audio chunk with user_id
149
+ dummy_audio = bytes([0] * 1024)
150
+ audio_msg = {
151
+ "user_id": "test-user",
152
+ "realtimeInput": {
153
+ "mediaChunks": [
154
+ {
155
+ "mimeType": "audio/pcm;rate=16000",
156
+ "data": dummy_audio.hex(),
157
+ }
158
+ ]
159
+ },
160
+ }
161
+ await send_message(websocket, audio_msg)
162
+ logger.info("Sent audio chunk")
163
+
164
+ # Send text message to complete the turn (matching frontend format)
165
+ text_msg = {
166
+ "content": {
167
+ "role": "user",
168
+ "parts": [{"text": "Test audio"}],
169
+ }
170
+ }
171
+ await send_message(websocket, text_msg)
172
+ logger.info("Sent text completion")
173
+
174
+ # Collect responses
175
+ responses = []
176
+ for _ in range(10):
177
+ try:
178
+ response = await receive_message(websocket, timeout=5.0)
179
+ responses.append(response)
180
+ logger.info(f"Received: {response}")
181
+
182
+ if isinstance(response, dict) and response.get("turn_complete"):
183
+ break
184
+ except TimeoutError:
185
+ break
186
+
187
+ # Verify we got responses
188
+ assert len(responses) > 0, "No responses received"
189
+
190
+ logger.info(f"Audio test passed. Received {len(responses)} responses")
191
+
192
+ finally:
193
+ await websocket.close()
194
+
195
+ except Exception as e:
196
+ logger.error(f"Audio test failed: {e}")
197
+ raise
198
+
199
+
200
+ def test_feedback_endpoint(server_fixture: subprocess.Popen[str]) -> None:
201
+ """Test the feedback endpoint."""
202
+ feedback_data = {
203
+ "score": 5,
204
+ "text": "Great response!",
205
+ "invocation_id": "test-run-123",
206
+ "user_id": "test-user",
207
+ "log_type": "feedback",
208
+ }
209
+
210
+ response = requests.post(FEEDBACK_URL, json=feedback_data, timeout=10)
211
+ assert response.status_code == 200
212
+ assert response.json() == {"status": "success"}
213
+ logger.info("Feedback endpoint test passed")
214
+ {%- else %}
14
215
 
15
216
  import json
16
217
  import logging
@@ -32,7 +233,7 @@ logging.basicConfig(level=logging.INFO)
32
233
  logger = logging.getLogger(__name__)
33
234
 
34
235
  BASE_URL = "http://127.0.0.1:8000/"
35
- {%- if "adk" in cookiecutter.tags %}
236
+ {%- if cookiecutter.is_adk %}
36
237
  STREAM_URL = BASE_URL + "run_sse"
37
238
  {%- else %}
38
239
  STREAM_URL = BASE_URL + "stream_messages"
@@ -86,7 +287,7 @@ def start_server() -> subprocess.Popen[str]:
86
287
  return process
87
288
 
88
289
 
89
- def wait_for_server(timeout: int = 60, interval: int = 1) -> bool:
290
+ def wait_for_server(timeout: int = 90, interval: int = 1) -> bool:
90
291
  """Wait for the server to be ready."""
91
292
  start_time = time.time()
92
293
  while time.time() - start_time < timeout:
@@ -124,7 +325,7 @@ def server_fixture(request: Any) -> Iterator[subprocess.Popen[str]]:
124
325
  def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
125
326
  """Test the chat stream functionality."""
126
327
  logger.info("Starting chat stream test")
127
- {% if "adk" in cookiecutter.tags %}
328
+ {% if cookiecutter.is_adk %}
128
329
  # Create session first
129
330
  user_id = "test_user_123"
130
331
  session_data = {"state": {"preferred_language": "English", "visit_count": 1}}
@@ -168,7 +369,7 @@ def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
168
369
  )
169
370
  assert response.status_code == 200
170
371
 
171
- {%- if "adk" in cookiecutter.tags %}
372
+ {%- if cookiecutter.is_adk %}
172
373
  # Parse SSE events from response
173
374
  events = []
174
375
  for line in response.iter_lines():
@@ -242,7 +443,7 @@ def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
242
443
  # Create sample feedback data
243
444
  feedback_data = {
244
445
  "score": 4,
245
- {%- if "adk" in cookiecutter.tags %}
446
+ {%- if cookiecutter.is_adk %}
246
447
  "invocation_id": str(uuid.uuid4()),
247
448
  {%- else %}
248
449
  "run_id": str(uuid.uuid4()),
@@ -285,3 +486,4 @@ def cleanup_agent_engine_sessions() -> None:
285
486
  except Exception as e:
286
487
  logger.warning(f"Failed to cleanup agent engine sessions: {e}")
287
488
  {%- endif %}
489
+ {%- endif %}
@@ -1,6 +1,87 @@
1
1
  # Robust Load Testing for Generative AI Applications
2
2
 
3
3
  This directory provides a comprehensive load testing framework for your Generative AI application, leveraging the power of [Locust](http://locust.io), a leading open-source load testing tool.
4
+ {%- if cookiecutter.agent_name == "adk_live" %}
5
+
6
+ ## Local Load Testing
7
+
8
+ Follow these steps to execute load tests on your local machine:
9
+
10
+ **1. Start the FastAPI Server:**
11
+
12
+ Launch the FastAPI server in a separate terminal:
13
+
14
+ ```bash
15
+ uv run uvicorn {{cookiecutter.agent_directory}}.server:app --host 0.0.0.0 --port 8000 --reload
16
+ ```
17
+
18
+ **2. (In another tab) Create virtual environment with Locust**
19
+ Using another terminal tab, This is suggested to avoid conflicts with the existing application python environment.
20
+
21
+ ```bash
22
+ python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1 websockets
23
+ ```
24
+
25
+ **3. Execute the Load Test:**
26
+ Trigger the Locust load test with the following command:
27
+
28
+ ```bash
29
+ locust -f tests/load_test/load_test.py \
30
+ -H http://127.0.0.1:8000 \
31
+ --headless \
32
+ -t 30s -u 2 -r 2 \
33
+ --csv=tests/load_test/.results/results \
34
+ --html=tests/load_test/.results/report.html
35
+ ```
36
+
37
+ This command initiates a 30-second load test, simulating 2 users spawning per second, reaching a maximum of 60 concurrent users.
38
+
39
+ **Results:**
40
+
41
+ Comprehensive CSV and HTML reports detailing the load test performance will be generated and saved in the `tests/load_test/.results` directory.
42
+
43
+ ## Remote Load Testing (Targeting Cloud Run)
44
+
45
+ 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.
46
+
47
+ **Prerequisites:**
48
+
49
+ - **Dependencies:** Ensure your environment has the same dependencies required for local testing.
50
+ - **Cloud Run Invoker Role:** You'll need the `roles/run.invoker` role to invoke the Cloud Run service.
51
+
52
+ **Steps:**
53
+
54
+ **1. Start Cloud Run Proxy:**
55
+
56
+ Start the proxy in a separate terminal to expose your Cloud Run service on localhost. The proxy automatically handles IAM authentication:
57
+
58
+ ```bash
59
+ gcloud run services proxy YOUR_SERVICE_NAME --port=8080 --region us-central1 --quiet
60
+ ```
61
+
62
+ Replace `YOUR_SERVICE_NAME` with your Cloud Run service name. The `--quiet` flag auto-approves component installation prompts. You can optionally specify `--tag` to target a specific traffic tag.
63
+
64
+ **2. (In another tab) Create virtual environment with Locust:**
65
+
66
+ Using another terminal tab:
67
+
68
+ ```bash
69
+ python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1 websockets
70
+ ```
71
+
72
+ **3. Execute the Load Test:**
73
+
74
+ Execute load tests against the proxied service. The proxy handles authentication automatically:
75
+
76
+ ```bash
77
+ locust -f tests/load_test/load_test.py \
78
+ -H http://127.0.0.1:8080 \
79
+ --headless \
80
+ -t 30s -u 2 -r 2 \
81
+ --csv=tests/load_test/.results/results \
82
+ --html=tests/load_test/.results/report.html
83
+ ```
84
+ {%- else %}
4
85
 
5
86
  ## Local Load Testing
6
87
 
@@ -81,3 +162,4 @@ locust -f tests/load_test/load_test.py \
81
162
  --csv=tests/load_test/.results/results \
82
163
  --html=tests/load_test/.results/report.html
83
164
  ```
165
+ {%- endif %}
@@ -0,0 +1,249 @@
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
+ {%- if cookiecutter.agent_name == "adk_live" %}
15
+
16
+ import json
17
+ import logging
18
+ import time
19
+ from typing import Any
20
+
21
+ from locust import User, between, task
22
+ from websockets.exceptions import WebSocketException
23
+ from websockets.sync.client import connect
24
+
25
+ logging.basicConfig(level=logging.INFO)
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class WebSocketUser(User):
30
+ """Simulates a user making websocket requests to the remote agent engine."""
31
+
32
+ wait_time = between(1, 3) # Wait 1-3 seconds between tasks
33
+ abstract = True
34
+
35
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
36
+ super().__init__(*args, **kwargs)
37
+ self.ws_url = (
38
+ self.host.replace("http://", "ws://").replace("https://", "wss://") + "/ws"
39
+ )
40
+
41
+ @task
42
+ def websocket_audio_conversation(self) -> None:
43
+ """Test a full websocket conversation with audio input."""
44
+ start_time = time.time()
45
+ response_count = 0
46
+ exception = None
47
+
48
+ try:
49
+ response_count = self._websocket_interaction()
50
+
51
+ # Mark as failure if we got no valid responses
52
+ if response_count == 0:
53
+ exception = Exception("No responses received from agent")
54
+
55
+ except WebSocketException as e:
56
+ exception = e
57
+ logger.error(f"WebSocket error: {e}")
58
+ except Exception as e:
59
+ exception = e
60
+ logger.error(f"Unexpected error: {e}")
61
+ finally:
62
+ total_time = int((time.time() - start_time) * 1000)
63
+
64
+ # Report the request metrics to Locust
65
+ self.environment.events.request.fire(
66
+ request_type="WS",
67
+ name="websocket_conversation",
68
+ response_time=total_time,
69
+ response_length=response_count * 100, # Approximate response size
70
+ response=None,
71
+ context={},
72
+ exception=exception,
73
+ )
74
+
75
+ def _websocket_interaction(self) -> int:
76
+ """Handle the websocket interaction and return response count."""
77
+ response_count = 0
78
+
79
+ with connect(self.ws_url, open_timeout=10, close_timeout=20) as websocket:
80
+ # Wait for setupComplete
81
+ setup_response = websocket.recv(timeout=10.0)
82
+ setup_data = json.loads(setup_response)
83
+ assert "setupComplete" in setup_data, (
84
+ f"Expected setupComplete, got {setup_data}"
85
+ )
86
+ logger.info("Received setupComplete")
87
+
88
+ # Send dummy audio chunk with user_id
89
+ dummy_audio = bytes([0] * 1024)
90
+ audio_msg = {
91
+ "user_id": "load-test-user",
92
+ "realtimeInput": {
93
+ "mediaChunks": [
94
+ {
95
+ "mimeType": "audio/pcm;rate=16000",
96
+ "data": dummy_audio.hex(),
97
+ }
98
+ ]
99
+ },
100
+ }
101
+ websocket.send(json.dumps(audio_msg))
102
+ logger.info("Sent audio chunk")
103
+
104
+ # Send text message to complete the turn
105
+ text_msg = {
106
+ "content": {
107
+ "role": "user",
108
+ "parts": [{"text": "Hello!"}],
109
+ }
110
+ }
111
+ websocket.send(json.dumps(text_msg))
112
+ logger.info("Sent text completion")
113
+
114
+ # Collect responses until turn_complete or timeout
115
+ for _ in range(20): # Max 20 responses
116
+ try:
117
+ response = websocket.recv(timeout=10.0)
118
+ response_data = json.loads(response)
119
+ response_count += 1
120
+ logger.debug(f"Received response: {response_data}")
121
+
122
+ if isinstance(response_data, dict) and response_data.get(
123
+ "turn_complete"
124
+ ):
125
+ logger.info(f"Turn complete after {response_count} responses")
126
+ break
127
+ except TimeoutError:
128
+ logger.info(f"Timeout after {response_count} responses")
129
+ break
130
+
131
+ return response_count
132
+
133
+
134
+ class RemoteAgentUser(WebSocketUser):
135
+ """User for testing remote agent engine deployment."""
136
+
137
+ # Set the host via command line: locust -f load_test.py --host=https://your-deployed-service.run.app
138
+ host = "http://localhost:8000" # Default for local testing
139
+ {%- else %}
140
+
141
+ import os
142
+ import time
143
+ {%- if cookiecutter.is_adk %}
144
+ import uuid
145
+
146
+ import requests
147
+ from locust import HttpUser, between, task
148
+ {%- else %}
149
+
150
+ from locust import HttpUser, between, task
151
+ {%- endif %}
152
+ {% if cookiecutter.is_adk %}
153
+ ENDPOINT = "/run_sse"
154
+ {% else %}
155
+ ENDPOINT = "/stream_messages"
156
+ {% endif %}
157
+
158
+ class ChatStreamUser(HttpUser):
159
+ """Simulates a user interacting with the chat stream API."""
160
+
161
+ wait_time = between(1, 3) # Wait 1-3 seconds between tasks
162
+
163
+ @task
164
+ def chat_stream(self) -> None:
165
+ """Simulates a chat stream interaction."""
166
+ headers = {"Content-Type": "application/json"}
167
+ if os.environ.get("_ID_TOKEN"):
168
+ headers["Authorization"] = f"Bearer {os.environ['_ID_TOKEN']}"
169
+ {%- if cookiecutter.is_adk %}
170
+ # Create session first
171
+ user_id = f"user_{uuid.uuid4()}"
172
+ session_data = {"state": {"preferred_language": "English", "visit_count": 1}}
173
+
174
+ session_url = f"{self.client.base_url}/apps/{{cookiecutter.agent_directory}}/users/{user_id}/sessions"
175
+ session_response = requests.post(
176
+ session_url,
177
+ headers=headers,
178
+ json=session_data,
179
+ timeout=10,
180
+ )
181
+
182
+ # Get session_id from response
183
+ session_id = session_response.json()["id"]
184
+
185
+ # Send chat message
186
+ data = {
187
+ "app_name": "{{cookiecutter.agent_directory}}",
188
+ "user_id": user_id,
189
+ "session_id": session_id,
190
+ "new_message": {
191
+ "role": "user",
192
+ "parts": [{"text": "Hello! Weather in New york?"}],
193
+ },
194
+ "streaming": True,
195
+ }
196
+ {%- else %}
197
+ data = {
198
+ "input": {
199
+ "messages": [
200
+ {"type": "human", "content": "Hello, AI!"},
201
+ {"type": "ai", "content": "Hello!"},
202
+ {"type": "human", "content": "Who are you?"},
203
+ ]
204
+ },
205
+ "config": {
206
+ "metadata": {"user_id": "test-user", "session_id": "test-session"}
207
+ },
208
+ }
209
+ {%- endif %}
210
+ start_time = time.time()
211
+
212
+ with self.client.post(
213
+ ENDPOINT,
214
+ name=f"{ENDPOINT} message",
215
+ headers=headers,
216
+ json=data,
217
+ catch_response=True,
218
+ stream=True,
219
+ params={"alt": "sse"},
220
+ ) as response:
221
+ if response.status_code == 200:
222
+ events = []
223
+ for line in response.iter_lines():
224
+ if line:
225
+ line_str = line.decode("utf-8")
226
+ events.append(line_str)
227
+
228
+ if "429 Too Many Requests" in line_str:
229
+ self.environment.events.request.fire(
230
+ request_type="POST",
231
+ name=f"{ENDPOINT} rate_limited 429s",
232
+ response_time=0,
233
+ response_length=len(line),
234
+ response=response,
235
+ context={},
236
+ )
237
+ end_time = time.time()
238
+ 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
+ )
247
+ else:
248
+ response.failure(f"Unexpected status code: {response.status_code}")
249
+ {%- endif %}