agent-starter-pack 0.15.6__py3-none-any.whl → 0.16.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 (102) hide show
  1. {agent_starter_pack-0.15.6.dist-info → agent_starter_pack-0.16.0.dist-info}/METADATA +2 -2
  2. {agent_starter_pack-0.15.6.dist-info → agent_starter_pack-0.16.0.dist-info}/RECORD +97 -95
  3. agents/adk_base/.template/templateconfig.yaml +1 -1
  4. agents/{live_api → adk_live}/.template/templateconfig.yaml +5 -7
  5. agents/adk_live/README.md +31 -0
  6. agents/adk_live/app/agent.py +48 -0
  7. agents/adk_live/tests/unit/test_dummy.py +38 -0
  8. agents/agentic_rag/.template/templateconfig.yaml +1 -1
  9. agents/crewai_coding_crew/app/agent.py +18 -57
  10. agents/langgraph_base_react/app/agent.py +7 -46
  11. llm.txt +1 -1
  12. src/base_template/GEMINI.md +1 -1
  13. src/base_template/Makefile +130 -61
  14. src/base_template/README.md +6 -6
  15. src/base_template/deployment/terraform/dev/apis.tf +1 -1
  16. src/base_template/deployment/terraform/dev/variables.tf +1 -1
  17. src/base_template/deployment/terraform/locals.tf +1 -1
  18. src/base_template/deployment/terraform/variables.tf +1 -1
  19. src/base_template/pyproject.toml +22 -21
  20. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +2 -2
  21. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +1 -1
  22. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +71 -8
  23. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +2 -2
  24. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/pr_checks.yaml +1 -1
  25. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +90 -8
  26. src/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +1 -1
  27. src/base_template/{{cookiecutter.agent_directory}}/utils/typing.py +4 -4
  28. src/cli/commands/create.py +1 -1
  29. src/cli/utils/template.py +12 -5
  30. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +205 -4
  31. src/deployment_targets/agent_engine/tests/load_test/README.md +47 -0
  32. src/deployment_targets/agent_engine/tests/load_test/load_test.py +132 -3
  33. src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +11 -3
  34. src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +5 -1
  35. src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/{% if cookiecutter.is_adk_live %}expose_app.py{% else %}unused_expose_app.py{% endif %} +461 -0
  36. src/deployment_targets/cloud_run/Dockerfile +3 -3
  37. src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +4 -4
  38. src/deployment_targets/cloud_run/deployment/terraform/service.tf +7 -7
  39. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +207 -5
  40. src/deployment_targets/cloud_run/tests/load_test/README.md +82 -0
  41. src/deployment_targets/cloud_run/tests/load_test/load_test.py +130 -3
  42. src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +178 -146
  43. src/frontends/{live_api_react → adk_live_react}/frontend/package-lock.json +39 -1007
  44. src/frontends/{live_api_react → adk_live_react}/frontend/package.json +1 -9
  45. src/frontends/{live_api_react → adk_live_react}/frontend/src/App.tsx +1 -1
  46. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/Logger.tsx +8 -3
  47. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/logger.scss +26 -0
  48. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/side-panel/SidePanel.tsx +11 -5
  49. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/side-panel/side-panel.scss +146 -115
  50. src/frontends/adk_live_react/frontend/src/components/transcription-preview/TranscriptionPreview.tsx +106 -0
  51. src/frontends/adk_live_react/frontend/src/components/transcription-preview/transcription-preview.scss +150 -0
  52. src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-live-api.ts +8 -2
  53. src/frontends/{live_api_react → adk_live_react}/frontend/src/multimodal-live-types.ts +38 -2
  54. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audio-recorder.ts +1 -1
  55. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audio-streamer.ts +1 -1
  56. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/multimodal-live-client.ts +204 -23
  57. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/utils.ts +27 -5
  58. src/frontends/streamlit/frontend/utils/local_chat_history.py +2 -0
  59. src/resources/idx/.idx/dev.nix +25 -11
  60. src/resources/idx/idx-template.json +1 -16
  61. src/resources/idx/idx-template.nix +2 -3
  62. src/resources/locks/uv-adk_base-agent_engine.lock +434 -349
  63. src/resources/locks/uv-adk_base-cloud_run.lock +502 -409
  64. src/resources/locks/uv-adk_live-agent_engine.lock +4189 -0
  65. src/resources/locks/{uv-live_api-cloud_run.lock → uv-adk_live-cloud_run.lock} +884 -2219
  66. src/resources/locks/uv-agentic_rag-agent_engine.lock +473 -388
  67. src/resources/locks/uv-agentic_rag-cloud_run.lock +557 -464
  68. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +498 -515
  69. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +898 -687
  70. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +455 -483
  71. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +910 -645
  72. src/utils/generate_locks.py +8 -4
  73. agents/live_api/README.md +0 -37
  74. agents/live_api/app/agent.py +0 -72
  75. agents/live_api/tests/integration/test_server_e2e.py +0 -260
  76. agents/live_api/tests/load_test/load_test.py +0 -40
  77. agents/live_api/tests/unit/test_server.py +0 -144
  78. {agent_starter_pack-0.15.6.dist-info → agent_starter_pack-0.16.0.dist-info}/WHEEL +0 -0
  79. {agent_starter_pack-0.15.6.dist-info → agent_starter_pack-0.16.0.dist-info}/entry_points.txt +0 -0
  80. {agent_starter_pack-0.15.6.dist-info → agent_starter_pack-0.16.0.dist-info}/licenses/LICENSE +0 -0
  81. /src/frontends/{live_api_react → adk_live_react}/frontend/public/favicon.ico +0 -0
  82. /src/frontends/{live_api_react → adk_live_react}/frontend/public/index.html +0 -0
  83. /src/frontends/{live_api_react → adk_live_react}/frontend/public/robots.txt +0 -0
  84. /src/frontends/{live_api_react → adk_live_react}/frontend/src/App.scss +0 -0
  85. /src/frontends/{live_api_react → adk_live_react}/frontend/src/App.test.tsx +0 -0
  86. /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/audio-pulse/AudioPulse.tsx +0 -0
  87. /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/audio-pulse/audio-pulse.scss +0 -0
  88. /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/mock-logs.ts +0 -0
  89. /src/frontends/{live_api_react → adk_live_react}/frontend/src/contexts/LiveAPIContext.tsx +0 -0
  90. /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-media-stream-mux.ts +0 -0
  91. /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-screen-capture.ts +0 -0
  92. /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-webcam.ts +0 -0
  93. /src/frontends/{live_api_react → adk_live_react}/frontend/src/index.css +0 -0
  94. /src/frontends/{live_api_react → adk_live_react}/frontend/src/index.tsx +0 -0
  95. /src/frontends/{live_api_react → adk_live_react}/frontend/src/react-app-env.d.ts +0 -0
  96. /src/frontends/{live_api_react → adk_live_react}/frontend/src/reportWebVitals.ts +0 -0
  97. /src/frontends/{live_api_react → adk_live_react}/frontend/src/setupTests.ts +0 -0
  98. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audioworklet-registry.ts +0 -0
  99. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/store-logger.ts +0 -0
  100. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/worklets/audio-processing.ts +0 -0
  101. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/worklets/vol-meter.ts +0 -0
  102. /src/frontends/{live_api_react → adk_live_react}/frontend/tsconfig.json +0 -0
@@ -20,7 +20,7 @@ steps:
20
20
  args:
21
21
  - -c
22
22
  - |
23
- cd data_ingestion && pip install uv==0.6.12 --user && cd data_ingestion_pipeline && \
23
+ cd data_ingestion && pip install uv==0.8.13 --user && cd data_ingestion_pipeline && \
24
24
  uv sync --locked && uv run python submit_pipeline.py
25
25
  env:
26
26
  - "PIPELINE_ROOT=${_PIPELINE_GCS_ROOT_PROD}"
@@ -63,7 +63,7 @@ steps:
63
63
  args:
64
64
  - "-c"
65
65
  - |
66
- pip install uv==0.6.12 --user && uv sync --locked
66
+ pip install uv==0.8.13 --user && uv sync --locked
67
67
  env:
68
68
  - 'PATH=/usr/local/bin:/usr/bin:~/.local/bin'
69
69
 
@@ -20,7 +20,7 @@ steps:
20
20
  args:
21
21
  - "-c"
22
22
  - |
23
- pip install uv==0.6.12 --user && uv sync --locked
23
+ pip install uv==0.8.13 --user && uv sync --locked
24
24
  env:
25
25
  - 'PATH=/usr/local/bin:/usr/bin:~/.local/bin'
26
26
 
@@ -20,7 +20,7 @@ steps:
20
20
  args:
21
21
  - -c
22
22
  - |
23
- cd data_ingestion && pip install uv==0.6.12 --user && cd data_ingestion_pipeline && \
23
+ cd data_ingestion && pip install uv==0.8.13 --user && cd data_ingestion_pipeline && \
24
24
  uv sync --locked && uv run python submit_pipeline.py
25
25
  env:
26
26
  - "PIPELINE_ROOT=${_PIPELINE_GCS_ROOT_STAGING}"
@@ -97,7 +97,7 @@ steps:
97
97
  args:
98
98
  - "-c"
99
99
  - |
100
- pip install uv==0.6.12 --user && uv sync --locked
100
+ pip install uv==0.8.13 --user && uv sync --locked
101
101
  env:
102
102
  - 'PATH=/usr/local/bin:/usr/bin:~/.local/bin'
103
103
 
@@ -127,31 +127,113 @@ steps:
127
127
  {%- endif %}
128
128
 
129
129
  # Load Testing
130
+ {%- if cookiecutter.deployment_target == 'cloud_run' and cookiecutter.agent_name == 'adk_live' %}
131
+ - name: "europe-west4-docker.pkg.dev/production-ai-template/starter-pack/e2e-tests"
132
+ id: load_test
133
+ entrypoint: /bin/bash
134
+ args:
135
+ - "-c"
136
+ - |
137
+ # Install load test dependencies
138
+ pip install locust==2.31.1 websockets
139
+
140
+ # Install Cloud Run proxy component
141
+ apt-get update && apt-get install -y google-cloud-cli-cloud-run-proxy
142
+
143
+ # Start Cloud Run proxy in background
144
+ gcloud run services proxy {{cookiecutter.project_name}} \
145
+ --port=8080 \
146
+ --region ${_REGION} \
147
+ --project ${_STAGING_PROJECT_ID} \
148
+ --quiet &
149
+ _PROXY_PID=$$!
150
+
151
+ # Wait for proxy to be ready
152
+ echo "Waiting for proxy to start..."
153
+ sleep 10
154
+
155
+ # Run load test and capture exit code
156
+ locust -f tests/load_test/load_test.py \
157
+ --headless \
158
+ -H http://127.0.0.1:8080 \
159
+ -t 30s -u 2 -r 2 \
160
+ --csv=tests/load_test/.results/results \
161
+ --html=tests/load_test/.results/report.html
162
+ _LOAD_TEST_EXIT_CODE=$$?
163
+
164
+ # Clean up proxy
165
+ kill $$_PROXY_PID || true
166
+
167
+ # Exit with load test result to fail build if tests failed
168
+ exit $$_LOAD_TEST_EXIT_CODE
169
+ {%- elif cookiecutter.deployment_target == 'cloud_run' %}
130
170
  - name: "python:3.11-slim"
131
171
  id: load_test
132
172
  entrypoint: /bin/bash
133
173
  args:
134
174
  - "-c"
135
175
  - |
136
- {%- if cookiecutter.deployment_target == 'cloud_run' %}
137
176
  export _ID_TOKEN=$(cat id_token.txt)
138
177
  export _STAGING_URL=$(cat staging_url.txt)
139
- {%- elif cookiecutter.deployment_target == 'agent_engine' %}
140
- export _AUTH_TOKEN=$(cat auth_token.txt)
141
- {%- endif %}
142
178
  pip install locust==2.31.1 --user
143
179
  locust -f tests/load_test/load_test.py \
144
180
  --headless \
145
- {%- if cookiecutter.deployment_target == 'cloud_run' %}
146
181
  -H $$_STAGING_URL \
147
182
  -t 30s -u 10 -r 0.5 \
183
+ --csv=tests/load_test/.results/results \
184
+ --html=tests/load_test/.results/report.html
185
+ env:
186
+ - 'PATH=/usr/local/bin:/usr/bin:~/.local/bin'
187
+ {%- elif cookiecutter.deployment_target == 'agent_engine' and cookiecutter.is_adk_live %}
188
+ - name: "python:3.11-slim"
189
+ id: load_test
190
+ entrypoint: /bin/bash
191
+ args:
192
+ - "-c"
193
+ - |
194
+ # Start expose app in remote mode (uses deployment_metadata.json by default)
195
+ uv run python -m {{cookiecutter.agent_directory}}.utils.expose_app --mode remote &
196
+ EXPOSE_PID=$$!
197
+
198
+ # Wait for expose app to be ready
199
+ sleep 10
200
+
201
+ # Run load test against local expose app
202
+ uv run --with locust==2.31.1 --with websockets locust -f tests/load_test/load_test.py \
203
+ -H http://127.0.0.1:8000 \
204
+ --headless \
205
+ -t 30s -u 2 -r 1 \
206
+ --csv=tests/load_test/.results/results \
207
+ --html=tests/load_test/.results/report.html
208
+ LOCUST_EXIT_CODE=$$?
209
+
210
+ # Stop expose app
211
+ kill $$EXPOSE_PID
212
+
213
+ # Exit with error if locust test failed
214
+ if [ $$LOCUST_EXIT_CODE -ne 0 ]; then
215
+ echo "Load test failed with exit code $$LOCUST_EXIT_CODE"
216
+ exit $$LOCUST_EXIT_CODE
217
+ fi
218
+ env:
219
+ - 'PATH=/usr/local/bin:/usr/bin:~/.local/bin'
148
220
  {%- elif cookiecutter.deployment_target == 'agent_engine' %}
221
+ - name: "python:3.11-slim"
222
+ id: load_test
223
+ entrypoint: /bin/bash
224
+ args:
225
+ - "-c"
226
+ - |
227
+ export _AUTH_TOKEN=$(cat auth_token.txt)
228
+ pip install locust==2.31.1 --user
229
+ locust -f tests/load_test/load_test.py \
230
+ --headless \
149
231
  -t 30s -u 2 -r 0.5 \
150
- {%- endif %}
151
232
  --csv=tests/load_test/.results/results \
152
233
  --html=tests/load_test/.results/report.html
153
234
  env:
154
235
  - 'PATH=/usr/local/bin:/usr/bin:~/.local/bin'
236
+ {%- endif %}
155
237
 
156
238
  # Export Load Test Results to GCS
157
239
  - name: gcr.io/cloud-builders/gcloud
@@ -86,7 +86,7 @@ class CloudTraceLoggingSpanExporter(CloudTraceSpanExporter):
86
86
  print(span_dict)
87
87
 
88
88
  # Log the span data to Google Cloud Logging
89
- {%- if "adk" in cookiecutter.tags %}
89
+ {%- if cookiecutter.is_adk %}
90
90
  self.logger.log_struct(
91
91
  span_dict,
92
92
  labels={
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- {%- if "adk" in cookiecutter.tags %}
15
+ {%- if cookiecutter.is_adk %}
16
16
  {%- if cookiecutter.deployment_target == 'cloud_run' %}
17
17
  import uuid
18
18
  from typing import (
@@ -57,7 +57,7 @@ from pydantic import (
57
57
  {%- endif %}
58
58
 
59
59
 
60
- {%- if "adk" in cookiecutter.tags %}
60
+ {%- if cookiecutter.is_adk %}
61
61
  {%- if cookiecutter.deployment_target == 'cloud_run' %}
62
62
 
63
63
 
@@ -103,7 +103,7 @@ class Feedback(BaseModel):
103
103
 
104
104
  score: int | float
105
105
  text: str | None = ""
106
- {%- if "adk" in cookiecutter.tags %}
106
+ {%- if cookiecutter.is_adk %}
107
107
  invocation_id: str
108
108
  {%- else %}
109
109
  run_id: str
@@ -111,7 +111,7 @@ class Feedback(BaseModel):
111
111
  log_type: Literal["feedback"] = "feedback"
112
112
  service_name: Literal["{{cookiecutter.project_name}}"] = "{{cookiecutter.project_name}}"
113
113
  user_id: str = ""
114
- {% if "adk" not in cookiecutter.tags %}
114
+ {% if not cookiecutter.is_adk %}
115
115
 
116
116
  def ensure_valid_config(config: RunnableConfig | None) -> RunnableConfig:
117
117
  """Ensures a valid RunnableConfig by setting defaults for missing fields."""
@@ -935,7 +935,7 @@ def set_gcp_project(project_id: str, set_quota_project: bool = True) -> None:
935
935
  capture_output=True,
936
936
  text=True,
937
937
  )
938
- except subprocess.CalledProcessError:
938
+ except subprocess.CalledProcessError as e:
939
939
  logging.debug(f"Setting quota project failed: {e.stderr}")
940
940
 
941
941
  console.print(f"> Successfully configured project: {project_id}")
src/cli/utils/template.py CHANGED
@@ -112,7 +112,7 @@ def get_available_agents(deployment_target: str | None = None) -> dict:
112
112
  # Define priority agents that should appear first
113
113
  PRIORITY_AGENTS = [
114
114
  "adk_base",
115
- "adk_gemini_fullstack",
115
+ "adk_live",
116
116
  "agentic_rag",
117
117
  "langgraph_base_react",
118
118
  ]
@@ -740,6 +740,8 @@ def process_template(
740
740
  ),
741
741
  "settings": settings,
742
742
  "tags": tags,
743
+ "is_adk": "adk" in tags,
744
+ "is_adk_live": "adk_live" in tags,
743
745
  "deployment_target": deployment_target or "",
744
746
  "cicd_runner": cicd_runner or "google_cloud_build",
745
747
  "session_type": session_type or "",
@@ -756,7 +758,12 @@ def process_template(
756
758
  "_copy_without_render": [
757
759
  "*.ipynb", # Don't render notebooks
758
760
  "*.json", # Don't render JSON files
759
- "frontend/*", # Don't render frontend directory
761
+ "*.tsx", # Don't render TypeScript React files
762
+ "*.ts", # Don't render TypeScript files
763
+ "*.jsx", # Don't render JavaScript React files
764
+ "*.js", # Don't render JavaScript files
765
+ "*.css", # Don't render CSS files
766
+ "frontend/**/*", # Don't render frontend directory recursively
760
767
  "notebooks/*", # Don't render notebooks directory
761
768
  ".git/*", # Don't render git directory
762
769
  "__pycache__/*", # Don't render cache
@@ -1109,12 +1116,12 @@ def should_exclude_path(
1109
1116
  path: pathlib.Path, agent_name: str, agent_directory: str = "app"
1110
1117
  ) -> bool:
1111
1118
  """Determine if a path should be excluded based on the agent type."""
1112
- if agent_name == "live_api":
1113
- # Exclude the unit test utils folder and agent utils folder for live_api
1119
+ if agent_name == "adk_live":
1120
+ # Exclude the unit test utils folder and agent utils folder for adk_live
1114
1121
  if "tests/unit/test_utils" in str(path) or f"{agent_directory}/utils" in str(
1115
1122
  path
1116
1123
  ):
1117
- logging.debug(f"Excluding path for live_api: {path}")
1124
+ logging.debug(f"Excluding path for adk_live: {path}")
1118
1125
  return True
1119
1126
  return False
1120
1127
 
@@ -11,11 +11,212 @@
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" %}
14
15
 
16
+ import asyncio
17
+ import json
15
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
16
25
 
17
26
  import pytest
18
- {%- if "adk" in cookiecutter.tags %}
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 using expose_app in local mode."""
46
+ command = [
47
+ sys.executable,
48
+ "-m",
49
+ "uvicorn",
50
+ "app.utils.expose_app: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
+ "run_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 %}
215
+
216
+ import logging
217
+
218
+ import pytest
219
+ {%- if cookiecutter.is_adk %}
19
220
  from google.adk.events.event import Event
20
221
 
21
222
  from {{cookiecutter.agent_directory}}.agent import root_agent
@@ -29,7 +230,7 @@ from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
29
230
  @pytest.fixture
30
231
  def agent_app() -> AgentEngineApp:
31
232
  """Fixture to create and set up AgentEngineApp instance"""
32
- {%- if "adk" in cookiecutter.tags %}
233
+ {%- if cookiecutter.is_adk %}
33
234
  app = AgentEngineApp(agent=root_agent)
34
235
  {%- else %}
35
236
  app = AgentEngineApp()
@@ -37,7 +238,7 @@ def agent_app() -> AgentEngineApp:
37
238
  app.set_up()
38
239
  return app
39
240
 
40
- {% if "adk" in cookiecutter.tags %}
241
+ {% if cookiecutter.is_adk %}
41
242
  @pytest.mark.asyncio
42
243
  async def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
43
244
  """
@@ -183,4 +384,4 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
183
384
  agent_app.register_feedback(invalid_feedback)
184
385
 
185
386
  logging.info("All assertions passed for agent feedback test")
186
- {% endif %}
387
+ {% endif %}{% endif %}
@@ -1,3 +1,49 @@
1
+ {%- if cookiecutter.agent_name == "adk_live" %}
2
+ # WebSocket Load Testing for Remote Agent Engine
3
+
4
+ This directory provides a comprehensive load testing framework for your Agent Engine application using WebSocket connections, leveraging the power of [Locust](http://locust.io), a leading open-source load testing tool.
5
+
6
+ The load test simulates realistic user interactions by:
7
+ - Establishing WebSocket connections
8
+ - Sending audio chunks in the proper `realtimeInput` format
9
+ - Sending text messages to complete turns
10
+ - Collecting and measuring responses until `turn_complete`
11
+
12
+ ## Load Testing with Remote Agent Engine
13
+
14
+ **1. Start the Expose App in Remote Mode:**
15
+
16
+ Launch the expose app server in a separate terminal, pointing to your deployed agent engine:
17
+
18
+ ```bash
19
+ uv run python -m app.utils.expose_app --mode remote --remote-id <your-agent-engine-id>
20
+ ```
21
+
22
+ Or if you have `deployment_metadata.json` in your project root:
23
+
24
+ ```bash
25
+ uv run python -m app.utils.expose_app --mode remote
26
+ ```
27
+
28
+ **2. Execute the Load Test:**
29
+
30
+ Using another terminal tab, trigger the Locust load test:
31
+
32
+ ```bash
33
+ uv run --with locust==2.31.1 --with websockets locust -f tests/load_test/load_test.py \
34
+ -H http://127.0.0.1:8000 \
35
+ --headless \
36
+ -t 30s -u 1 -r 1 \
37
+ --csv=tests/load_test/.results/results \
38
+ --html=tests/load_test/.results/report.html
39
+ ```
40
+
41
+ This command initiates a 30-second load test with 1 concurrent user.
42
+
43
+ **Results:**
44
+
45
+ Comprehensive CSV and HTML reports detailing the load test performance will be generated and saved in the `tests/load_test/.results` directory.
46
+ {%- else %}
1
47
  # Robust Load Testing for Generative AI Applications
2
48
 
3
49
  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.
@@ -34,4 +80,5 @@ Follow these steps to execute load tests:
34
80
  ```
35
81
 
36
82
  This command initiates a 30-second load test, simulating 2 users spawning per second, reaching a maximum of 10 concurrent users.
83
+ {%- endif %}
37
84