agent-starter-pack 0.15.7__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.
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/METADATA +2 -2
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/RECORD +96 -94
- agents/adk_base/.template/templateconfig.yaml +1 -1
- agents/{live_api → adk_live}/.template/templateconfig.yaml +5 -7
- agents/adk_live/README.md +31 -0
- agents/adk_live/app/agent.py +48 -0
- agents/adk_live/tests/unit/test_dummy.py +38 -0
- agents/agentic_rag/.template/templateconfig.yaml +1 -1
- agents/crewai_coding_crew/app/agent.py +18 -57
- agents/langgraph_base_react/app/agent.py +7 -46
- llm.txt +1 -1
- src/base_template/GEMINI.md +1 -1
- src/base_template/Makefile +130 -61
- src/base_template/README.md +6 -6
- src/base_template/deployment/terraform/dev/apis.tf +1 -1
- src/base_template/deployment/terraform/dev/variables.tf +1 -1
- src/base_template/deployment/terraform/locals.tf +1 -1
- src/base_template/deployment/terraform/variables.tf +1 -1
- src/base_template/pyproject.toml +22 -21
- src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +2 -2
- src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +1 -1
- src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +71 -8
- src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +2 -2
- src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/pr_checks.yaml +1 -1
- src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +90 -8
- src/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +1 -1
- src/base_template/{{cookiecutter.agent_directory}}/utils/typing.py +4 -4
- src/cli/utils/template.py +12 -5
- src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +205 -4
- src/deployment_targets/agent_engine/tests/load_test/README.md +47 -0
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +132 -3
- src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +11 -3
- src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +5 -1
- 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
- src/deployment_targets/cloud_run/Dockerfile +3 -3
- src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +4 -4
- src/deployment_targets/cloud_run/deployment/terraform/service.tf +7 -7
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +207 -5
- src/deployment_targets/cloud_run/tests/load_test/README.md +82 -0
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +130 -3
- src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +178 -146
- src/frontends/{live_api_react → adk_live_react}/frontend/package-lock.json +39 -1007
- src/frontends/{live_api_react → adk_live_react}/frontend/package.json +1 -9
- src/frontends/{live_api_react → adk_live_react}/frontend/src/App.tsx +1 -1
- src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/Logger.tsx +8 -3
- src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/logger.scss +26 -0
- src/frontends/{live_api_react → adk_live_react}/frontend/src/components/side-panel/SidePanel.tsx +11 -5
- src/frontends/{live_api_react → adk_live_react}/frontend/src/components/side-panel/side-panel.scss +146 -115
- src/frontends/adk_live_react/frontend/src/components/transcription-preview/TranscriptionPreview.tsx +106 -0
- src/frontends/adk_live_react/frontend/src/components/transcription-preview/transcription-preview.scss +150 -0
- src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-live-api.ts +8 -2
- src/frontends/{live_api_react → adk_live_react}/frontend/src/multimodal-live-types.ts +38 -2
- src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audio-recorder.ts +1 -1
- src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audio-streamer.ts +1 -1
- src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/multimodal-live-client.ts +204 -23
- src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/utils.ts +27 -5
- src/frontends/streamlit/frontend/utils/local_chat_history.py +2 -0
- src/resources/idx/.idx/dev.nix +25 -11
- src/resources/idx/idx-template.json +1 -16
- src/resources/idx/idx-template.nix +2 -3
- src/resources/locks/uv-adk_base-agent_engine.lock +434 -349
- src/resources/locks/uv-adk_base-cloud_run.lock +502 -409
- src/resources/locks/uv-adk_live-agent_engine.lock +4189 -0
- src/resources/locks/{uv-live_api-cloud_run.lock → uv-adk_live-cloud_run.lock} +884 -2219
- src/resources/locks/uv-agentic_rag-agent_engine.lock +473 -388
- src/resources/locks/uv-agentic_rag-cloud_run.lock +557 -464
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +498 -515
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +898 -687
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +455 -483
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +910 -645
- src/utils/generate_locks.py +8 -4
- agents/live_api/README.md +0 -37
- agents/live_api/app/agent.py +0 -72
- agents/live_api/tests/integration/test_server_e2e.py +0 -260
- agents/live_api/tests/load_test/load_test.py +0 -40
- agents/live_api/tests/unit/test_server.py +0 -144
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/entry_points.txt +0 -0
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/licenses/LICENSE +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/public/favicon.ico +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/public/index.html +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/public/robots.txt +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/App.scss +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/App.test.tsx +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/audio-pulse/AudioPulse.tsx +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/audio-pulse/audio-pulse.scss +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/mock-logs.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/contexts/LiveAPIContext.tsx +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-media-stream-mux.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-screen-capture.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-webcam.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/index.css +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/index.tsx +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/react-app-env.d.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/reportWebVitals.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/setupTests.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audioworklet-registry.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/store-logger.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/worklets/audio-processing.ts +0 -0
- /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/worklets/vol-meter.ts +0 -0
- /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.
|
|
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.
|
|
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
|
-
cd data_ingestion && pip install uv==0.
|
|
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.
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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."""
|
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
|
-
"
|
|
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
|
-
"
|
|
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 == "
|
|
1113
|
-
# Exclude the unit test utils folder and agent utils folder for
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|