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.
- {agents → agent_starter_pack/agents}/adk_base/.template/templateconfig.yaml +1 -1
- {agents/live_api → agent_starter_pack/agents/adk_live}/.template/templateconfig.yaml +5 -7
- agent_starter_pack/agents/adk_live/README.md +32 -0
- agent_starter_pack/agents/adk_live/app/agent.py +48 -0
- agent_starter_pack/agents/adk_live/tests/unit/test_dummy.py +38 -0
- {agents → agent_starter_pack/agents}/agentic_rag/.template/templateconfig.yaml +1 -1
- agent_starter_pack/agents/crewai_coding_crew/app/agent.py +47 -0
- agent_starter_pack/agents/langgraph_base_react/app/agent.py +34 -0
- {src → agent_starter_pack}/base_template/GEMINI.md +1 -1
- {src → agent_starter_pack}/base_template/Makefile +130 -61
- {src → agent_starter_pack}/base_template/README.md +6 -6
- {src → agent_starter_pack}/base_template/deployment/terraform/dev/apis.tf +1 -1
- {src → agent_starter_pack}/base_template/deployment/terraform/dev/log_sinks.tf +31 -25
- {src → agent_starter_pack}/base_template/deployment/terraform/dev/providers.tf +1 -1
- {src → agent_starter_pack}/base_template/deployment/terraform/dev/variables.tf +1 -1
- {src → agent_starter_pack}/base_template/deployment/terraform/github.tf +14 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/locals.tf +1 -1
- {src → agent_starter_pack}/base_template/deployment/terraform/log_sinks.tf +37 -28
- {src → agent_starter_pack}/base_template/deployment/terraform/providers.tf +1 -1
- {src → agent_starter_pack}/base_template/deployment/terraform/variables.tf +1 -1
- {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
- {src → agent_starter_pack}/base_template/pyproject.toml +22 -21
- {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +5 -5
- {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +3 -3
- {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +74 -11
- {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +6 -6
- {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/pr_checks.yaml +4 -4
- {src → agent_starter_pack}/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +95 -13
- {src → agent_starter_pack}/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +1 -1
- {src → agent_starter_pack}/base_template/{{cookiecutter.agent_directory}}/utils/typing.py +4 -4
- {src → agent_starter_pack}/cli/commands/setup_cicd.py +1 -1
- {src → agent_starter_pack}/cli/main.py +2 -2
- {src → agent_starter_pack}/cli/utils/gcp.py +1 -1
- {src → agent_starter_pack}/cli/utils/remote_template.py +12 -9
- {src → agent_starter_pack}/cli/utils/template.py +19 -15
- agent_starter_pack/deployment_targets/agent_engine/deployment/terraform/{% if not cookiecutter.is_adk_live %}service.tf{% else %}unused_service.tf{% endif %} +82 -0
- agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +387 -0
- agent_starter_pack/deployment_targets/agent_engine/tests/load_test/README.md +84 -0
- agent_starter_pack/deployment_targets/agent_engine/tests/load_test/load_test.py +255 -0
- {src → agent_starter_pack}/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +40 -14
- {src → agent_starter_pack}/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +13 -4
- 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
- {src → agent_starter_pack}/deployment_targets/cloud_run/Dockerfile +3 -3
- {src → agent_starter_pack}/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +4 -4
- {src → agent_starter_pack}/deployment_targets/cloud_run/deployment/terraform/service.tf +7 -7
- {src → agent_starter_pack}/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +207 -5
- {src → agent_starter_pack}/deployment_targets/cloud_run/tests/load_test/README.md +82 -0
- agent_starter_pack/deployment_targets/cloud_run/tests/load_test/load_test.py +249 -0
- {src → agent_starter_pack}/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +190 -146
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/package-lock.json +39 -1007
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/package.json +1 -9
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/App.tsx +1 -1
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/logger/Logger.tsx +8 -3
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/logger/logger.scss +26 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/side-panel/SidePanel.tsx +11 -5
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/side-panel/side-panel.scss +146 -115
- agent_starter_pack/frontends/adk_live_react/frontend/src/components/transcription-preview/TranscriptionPreview.tsx +106 -0
- agent_starter_pack/frontends/adk_live_react/frontend/src/components/transcription-preview/transcription-preview.scss +150 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-live-api.ts +8 -2
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/multimodal-live-types.ts +38 -2
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/audio-recorder.ts +1 -1
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/audio-streamer.ts +1 -1
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/multimodal-live-client.ts +204 -23
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/utils.ts +27 -5
- {src → agent_starter_pack}/frontends/streamlit/frontend/utils/local_chat_history.py +2 -0
- {src → agent_starter_pack}/resources/docs/adk-cheatsheet.md +5 -5
- agent_starter_pack/resources/idx/.idx/dev.nix +64 -0
- agent_starter_pack/resources/idx/idx-template.json +6 -0
- {src → agent_starter_pack}/resources/idx/idx-template.nix +2 -3
- {src → agent_starter_pack}/resources/locks/uv-adk_base-agent_engine.lock +1079 -954
- {src → agent_starter_pack}/resources/locks/uv-adk_base-cloud_run.lock +1441 -1309
- agent_starter_pack/resources/locks/uv-adk_live-agent_engine.lock +4229 -0
- agent_starter_pack/resources/locks/uv-adk_live-cloud_run.lock +4822 -0
- {src → agent_starter_pack}/resources/locks/uv-agentic_rag-agent_engine.lock +1107 -997
- {src → agent_starter_pack}/resources/locks/uv-agentic_rag-cloud_run.lock +1485 -1368
- {src → agent_starter_pack}/resources/locks/uv-crewai_coding_crew-agent_engine.lock +1294 -1297
- {src → agent_starter_pack}/resources/locks/uv-crewai_coding_crew-cloud_run.lock +2028 -1807
- {src → agent_starter_pack}/resources/locks/uv-langgraph_base_react-agent_engine.lock +1176 -1197
- {src → agent_starter_pack}/resources/locks/uv-langgraph_base_react-cloud_run.lock +1947 -1679
- {src → agent_starter_pack}/utils/generate_locks.py +12 -7
- {src → agent_starter_pack}/utils/lock_utils.py +2 -2
- {src → agent_starter_pack}/utils/watch_and_rebuild.py +1 -1
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.17.0.dist-info}/METADATA +17 -18
- agent_starter_pack-0.17.0.dist-info/RECORD +179 -0
- agent_starter_pack-0.17.0.dist-info/entry_points.txt +2 -0
- llm.txt +1 -1
- agent_starter_pack-0.15.7.dist-info/RECORD +0 -176
- agent_starter_pack-0.15.7.dist-info/entry_points.txt +0 -2
- agents/crewai_coding_crew/app/agent.py +0 -86
- agents/langgraph_base_react/app/agent.py +0 -73
- 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
- src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +0 -186
- src/deployment_targets/agent_engine/tests/load_test/README.md +0 -37
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +0 -126
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +0 -122
- src/resources/idx/.idx/dev.nix +0 -50
- src/resources/idx/idx-template.json +0 -21
- src/resources/locks/uv-live_api-cloud_run.lock +0 -6118
- {agents → agent_starter_pack/agents}/README.md +0 -0
- {agents → agent_starter_pack/agents}/adk_base/README.md +0 -0
- {agents → agent_starter_pack/agents}/adk_base/app/__init__.py +0 -0
- {agents → agent_starter_pack/agents}/adk_base/app/agent.py +0 -0
- {agents → agent_starter_pack/agents}/adk_base/notebooks/adk_app_testing.ipynb +0 -0
- {agents → agent_starter_pack/agents}/adk_base/notebooks/evaluating_adk_agent.ipynb +0 -0
- {agents → agent_starter_pack/agents}/adk_base/tests/integration/test_agent.py +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/README.md +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/app/__init__.py +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/app/agent.py +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/app/retrievers.py +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/app/templates.py +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/notebooks/adk_app_testing.ipynb +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/notebooks/evaluating_adk_agent.ipynb +0 -0
- {agents → agent_starter_pack/agents}/agentic_rag/tests/integration/test_agent.py +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/.template/templateconfig.yaml +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/README.md +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/app/crew/config/agents.yaml +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/app/crew/config/tasks.yaml +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/app/crew/crew.py +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +0 -0
- {agents → agent_starter_pack/agents}/crewai_coding_crew/tests/integration/test_agent.py +0 -0
- {agents → agent_starter_pack/agents}/langgraph_base_react/.template/templateconfig.yaml +0 -0
- {agents → agent_starter_pack/agents}/langgraph_base_react/README.md +0 -0
- {agents → agent_starter_pack/agents}/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +0 -0
- {agents → agent_starter_pack/agents}/langgraph_base_react/tests/integration/test_agent.py +0 -0
- {src → agent_starter_pack}/base_template/.gitignore +0 -0
- {src → agent_starter_pack}/base_template/deployment/README.md +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/apis.tf +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/dev/iam.tf +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/dev/storage.tf +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/dev/vars/env.tfvars +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/iam.tf +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/service_accounts.tf +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/storage.tf +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/vars/env.tfvars +0 -0
- {src → agent_starter_pack}/base_template/deployment/terraform/{% if cookiecutter.cicd_runner == 'github_actions' %}wif.tf{% else %}unused_wif.tf{% endif %} +0 -0
- {src → agent_starter_pack}/base_template/tests/unit/test_dummy.py +0 -0
- {src → agent_starter_pack}/base_template/{{cookiecutter.agent_directory}}/utils/gcs.py +0 -0
- {src → agent_starter_pack}/cli/commands/create.py +0 -0
- {src → agent_starter_pack}/cli/commands/enhance.py +0 -0
- {src → agent_starter_pack}/cli/commands/list.py +0 -0
- {src → agent_starter_pack}/cli/utils/__init__.py +0 -0
- {src → agent_starter_pack}/cli/utils/cicd.py +0 -0
- {src → agent_starter_pack}/cli/utils/datastores.py +0 -0
- {src → agent_starter_pack}/cli/utils/logging.py +0 -0
- {src → agent_starter_pack}/cli/utils/version.py +0 -0
- {src → agent_starter_pack}/data_ingestion/README.md +0 -0
- {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +0 -0
- {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/components/process_data.py +0 -0
- {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/pipeline.py +0 -0
- {src → agent_starter_pack}/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +0 -0
- {src → agent_starter_pack}/data_ingestion/pyproject.toml +0 -0
- {src → agent_starter_pack}/data_ingestion/uv.lock +0 -0
- {src → agent_starter_pack}/deployment_targets/agent_engine/deployment_metadata.json +0 -0
- {src → agent_starter_pack}/deployment_targets/agent_engine/notebooks/intro_agent_engine.ipynb +0 -0
- {src → agent_starter_pack}/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
- {src → agent_starter_pack}/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/public/favicon.ico +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/public/index.html +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/public/robots.txt +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/App.scss +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/App.test.tsx +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/audio-pulse/AudioPulse.tsx +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/audio-pulse/audio-pulse.scss +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/components/logger/mock-logs.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/contexts/LiveAPIContext.tsx +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-media-stream-mux.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-screen-capture.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/hooks/use-webcam.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/index.css +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/index.tsx +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/react-app-env.d.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/reportWebVitals.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/setupTests.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/audioworklet-registry.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/store-logger.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/worklets/audio-processing.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/src/utils/worklets/vol-meter.ts +0 -0
- {src/frontends/live_api_react → agent_starter_pack/frontends/adk_live_react}/frontend/tsconfig.json +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/side_bar.py +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/streamlit_app.py +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/style/app_markdown.py +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/utils/chat_utils.py +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/utils/message_editing.py +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/utils/multimodal_utils.py +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/utils/stream_handler.py +0 -0
- {src → agent_starter_pack}/frontends/streamlit/frontend/utils/title_summary.py +0 -0
- {src → agent_starter_pack}/resources/containers/data_processing/Dockerfile +0 -0
- {src → agent_starter_pack}/resources/containers/e2e-tests/Dockerfile +0 -0
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.17.0.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.17.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
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 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 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 %}
|
|
220
|
+
from google.adk.events.event import Event
|
|
221
|
+
|
|
222
|
+
from {{cookiecutter.agent_directory}}.agent import root_agent
|
|
223
|
+
from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
|
|
224
|
+
{%- else %}
|
|
225
|
+
|
|
226
|
+
from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
|
|
227
|
+
{%- endif %}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@pytest.fixture
|
|
231
|
+
def agent_app() -> AgentEngineApp:
|
|
232
|
+
"""Fixture to create and set up AgentEngineApp instance"""
|
|
233
|
+
{%- if cookiecutter.is_adk %}
|
|
234
|
+
app = AgentEngineApp(agent=root_agent)
|
|
235
|
+
{%- else %}
|
|
236
|
+
app = AgentEngineApp()
|
|
237
|
+
{%- endif %}
|
|
238
|
+
app.set_up()
|
|
239
|
+
return app
|
|
240
|
+
|
|
241
|
+
{% if cookiecutter.is_adk %}
|
|
242
|
+
@pytest.mark.asyncio
|
|
243
|
+
async def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Integration test for the agent stream query functionality.
|
|
246
|
+
Tests that the agent returns valid streaming responses.
|
|
247
|
+
"""
|
|
248
|
+
# Create message and events for the async_stream_query
|
|
249
|
+
message = "What's the weather in San Francisco?"
|
|
250
|
+
events = []
|
|
251
|
+
async for event in agent_app.async_stream_query(message=message, user_id="test"):
|
|
252
|
+
events.append(event)
|
|
253
|
+
assert len(events) > 0, "Expected at least one chunk in response"
|
|
254
|
+
|
|
255
|
+
# Check for valid content in the response
|
|
256
|
+
has_text_content = False
|
|
257
|
+
for event in events:
|
|
258
|
+
validated_event = Event.model_validate(event)
|
|
259
|
+
content = validated_event.content
|
|
260
|
+
if (
|
|
261
|
+
content is not None
|
|
262
|
+
and content.parts
|
|
263
|
+
and any(part.text for part in content.parts)
|
|
264
|
+
):
|
|
265
|
+
has_text_content = True
|
|
266
|
+
break
|
|
267
|
+
|
|
268
|
+
assert has_text_content, "Expected at least one event with text content"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_agent_feedback(agent_app: AgentEngineApp) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Integration test for the agent feedback functionality.
|
|
274
|
+
Tests that feedback can be registered successfully.
|
|
275
|
+
"""
|
|
276
|
+
feedback_data = {
|
|
277
|
+
"score": 5,
|
|
278
|
+
"text": "Great response!",
|
|
279
|
+
"invocation_id": "test-run-123",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# Should not raise any exceptions
|
|
283
|
+
agent_app.register_feedback(feedback_data)
|
|
284
|
+
|
|
285
|
+
# Test invalid feedback
|
|
286
|
+
with pytest.raises(ValueError):
|
|
287
|
+
invalid_feedback = {
|
|
288
|
+
"score": "invalid", # Score must be numeric
|
|
289
|
+
"text": "Bad feedback",
|
|
290
|
+
"invocation_id": "test-run-123",
|
|
291
|
+
}
|
|
292
|
+
agent_app.register_feedback(invalid_feedback)
|
|
293
|
+
|
|
294
|
+
logging.info("All assertions passed for agent feedback test")
|
|
295
|
+
{% else %}
|
|
296
|
+
def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Integration test for the agent stream query functionality.
|
|
299
|
+
Tests that the agent returns valid streaming responses.
|
|
300
|
+
"""
|
|
301
|
+
input_dict = {
|
|
302
|
+
"messages": [
|
|
303
|
+
{"type": "human", "content": "Test message"},
|
|
304
|
+
],
|
|
305
|
+
"user_id": "test-user",
|
|
306
|
+
"session_id": "test-session",
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
events = list(agent_app.stream_query(input=input_dict))
|
|
310
|
+
|
|
311
|
+
assert len(events) > 0, "Expected at least one chunk in response"
|
|
312
|
+
|
|
313
|
+
# Verify each event is a tuple of message and metadata
|
|
314
|
+
for event in events:
|
|
315
|
+
assert isinstance(event, list), "Event should be a list"
|
|
316
|
+
assert len(event) == 2, "Event should contain message and metadata"
|
|
317
|
+
message, _ = event
|
|
318
|
+
|
|
319
|
+
# Verify message structure
|
|
320
|
+
assert isinstance(message, dict), "Message should be a dictionary"
|
|
321
|
+
assert message["type"] == "constructor"
|
|
322
|
+
assert "kwargs" in message, "Constructor message should have kwargs"
|
|
323
|
+
|
|
324
|
+
# Verify at least one message has content
|
|
325
|
+
has_content = False
|
|
326
|
+
for event in events:
|
|
327
|
+
message = event[0]
|
|
328
|
+
if message.get("type") == "constructor" and "content" in message["kwargs"]:
|
|
329
|
+
has_content = True
|
|
330
|
+
break
|
|
331
|
+
assert has_content, "At least one message should have content"
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_agent_query(agent_app: AgentEngineApp) -> None:
|
|
335
|
+
"""
|
|
336
|
+
Integration test for the agent query functionality.
|
|
337
|
+
Tests that the agent returns valid responses.
|
|
338
|
+
"""
|
|
339
|
+
input_dict = {
|
|
340
|
+
"messages": [
|
|
341
|
+
{"type": "human", "content": "Test message"},
|
|
342
|
+
],
|
|
343
|
+
"user_id": "test-user",
|
|
344
|
+
"session_id": "test-session",
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
response = agent_app.query(input=input_dict)
|
|
348
|
+
|
|
349
|
+
# Basic response validation
|
|
350
|
+
assert isinstance(response, dict), "Response should be a dictionary"
|
|
351
|
+
assert "messages" in response, "Response should contain messages"
|
|
352
|
+
assert len(response["messages"]) > 0, "Response should have at least one message"
|
|
353
|
+
|
|
354
|
+
# Validate last message is AI response with content
|
|
355
|
+
message = response["messages"][-1]
|
|
356
|
+
kwargs = message["kwargs"]
|
|
357
|
+
assert kwargs["type"] == "ai", "Last message should be AI response"
|
|
358
|
+
assert len(kwargs["content"]) > 0, "AI message content should not be empty"
|
|
359
|
+
|
|
360
|
+
logging.info("All assertions passed for agent query test")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_agent_feedback(agent_app: AgentEngineApp) -> None:
|
|
364
|
+
"""
|
|
365
|
+
Integration test for the agent feedback functionality.
|
|
366
|
+
Tests that feedback can be registered successfully.
|
|
367
|
+
"""
|
|
368
|
+
feedback_data = {
|
|
369
|
+
"score": 5,
|
|
370
|
+
"text": "Great response!",
|
|
371
|
+
"run_id": "test-run-123",
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Should not raise any exceptions
|
|
375
|
+
agent_app.register_feedback(feedback_data)
|
|
376
|
+
|
|
377
|
+
# Test invalid feedback
|
|
378
|
+
with pytest.raises(ValueError):
|
|
379
|
+
invalid_feedback = {
|
|
380
|
+
"score": "invalid", # Score must be numeric
|
|
381
|
+
"text": "Bad feedback",
|
|
382
|
+
"run_id": "test-run-123",
|
|
383
|
+
}
|
|
384
|
+
agent_app.register_feedback(invalid_feedback)
|
|
385
|
+
|
|
386
|
+
logging.info("All assertions passed for agent feedback test")
|
|
387
|
+
{% endif %}{% endif %}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 %}
|
|
47
|
+
# Robust Load Testing for Generative AI Applications
|
|
48
|
+
|
|
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.
|
|
50
|
+
|
|
51
|
+
## Load Testing
|
|
52
|
+
|
|
53
|
+
Before running load tests, ensure you have deployed the backend remotely.
|
|
54
|
+
|
|
55
|
+
Follow these steps to execute load tests:
|
|
56
|
+
|
|
57
|
+
**1. Deploy the Backend Remotely:**
|
|
58
|
+
```bash
|
|
59
|
+
gcloud config set project <your-dev-project-id>
|
|
60
|
+
make backend
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**2. Create a Virtual Environment for Locust:**
|
|
64
|
+
It's recommended to use a separate terminal tab and create a virtual environment for Locust to avoid conflicts with your application's Python environment.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python3 -m venv .locust_env && source .locust_env/bin/activate && pip install locust==2.31.1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**3. Execute the Load Test:**
|
|
71
|
+
Trigger the Locust load test with the following command:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
export _AUTH_TOKEN=$(gcloud auth print-access-token -q)
|
|
75
|
+
locust -f tests/load_test/load_test.py \
|
|
76
|
+
--headless \
|
|
77
|
+
-t 30s -u 5 -r 2 \
|
|
78
|
+
--csv=tests/load_test/.results/results \
|
|
79
|
+
--html=tests/load_test/.results/report.html
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This command initiates a 30-second load test, simulating 2 users spawning per second, reaching a maximum of 10 concurrent users.
|
|
83
|
+
{%- endif %}
|
|
84
|
+
|