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
src/utils/generate_locks.py
CHANGED
|
@@ -22,7 +22,7 @@ import subprocess
|
|
|
22
22
|
import tempfile
|
|
23
23
|
|
|
24
24
|
import click
|
|
25
|
-
from jinja2 import Template
|
|
25
|
+
from jinja2 import StrictUndefined, Template
|
|
26
26
|
from lock_utils import get_agent_configs, get_lock_filename
|
|
27
27
|
|
|
28
28
|
|
|
@@ -55,15 +55,19 @@ def generate_pyproject(
|
|
|
55
55
|
extra_dependencies: List of additional dependencies from .templateconfig.yaml
|
|
56
56
|
"""
|
|
57
57
|
with open(template_path, encoding="utf-8") as f:
|
|
58
|
-
template = Template(f.read(), trim_blocks=True, lstrip_blocks=True)
|
|
58
|
+
template = Template(f.read(), trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined)
|
|
59
59
|
|
|
60
60
|
# Convert list to proper format for template
|
|
61
|
+
tags = list(config.get("tags", []))
|
|
61
62
|
context = {
|
|
62
63
|
"cookiecutter": {
|
|
63
64
|
"project_name": "locked-template",
|
|
64
65
|
"deployment_target": deployment_target,
|
|
65
|
-
"extra_dependencies": list(config.get("extra_dependencies",[])),
|
|
66
|
-
"tags":
|
|
66
|
+
"extra_dependencies": list(config.get("extra_dependencies", [])),
|
|
67
|
+
"tags": tags,
|
|
68
|
+
"is_adk": "adk" in tags,
|
|
69
|
+
"is_adk_live": "adk_live" in tags,
|
|
70
|
+
"agent_directory": config.get("agent_directory", "app"),
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
agents/live_api/README.md
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# Multimodal Live Agent
|
|
2
|
-
|
|
3
|
-
This pattern showcases a real-time conversational agent powered by Google Gemini. The agent handles audio, video, and text interactions while leveraging tool calling capabilities for enhanced responses.
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
**Key components:**
|
|
8
|
-
|
|
9
|
-
- **Python Backend** (in `app/` folder): A production-ready server built with [FastAPI](https://fastapi.tiangolo.com/) and [google-genai](https://googleapis.github.io/python-genai/) that features:
|
|
10
|
-
|
|
11
|
-
- **Real-time bidirectional communication** via WebSockets between the frontend and Gemini model
|
|
12
|
-
- **Integrated tool calling** with a weather information tool for demonstrating external data retrieval
|
|
13
|
-
- **Production-grade reliability** with retry logic and automatic reconnection capabilities
|
|
14
|
-
- **Deployment flexibility** supporting both AI Studio and Vertex AI endpoints
|
|
15
|
-
- **Feedback logging endpoint** for collecting user interactions
|
|
16
|
-
|
|
17
|
-
- **React Frontend** (in `frontend/` folder): Extends the [Multimodal live API Web Console](https://github.com/google-gemini/multimodal-live-api-web-console), with added features like **custom URLs** and **feedback collection**.
|
|
18
|
-
|
|
19
|
-

|
|
20
|
-
|
|
21
|
-
Once both the backend and frontend are running, click the play button in the frontend UI to establish a connection with the backend. You can now interact with the Multimodal Live Agent! You can try asking questions such as "What's the weather like in San Francisco?" to see the agent use its weather information tool.
|
|
22
|
-
|
|
23
|
-
## Additional Resources for Multimodal Live API
|
|
24
|
-
|
|
25
|
-
Explore these resources to learn more about the Multimodal Live API and see examples of its usage:
|
|
26
|
-
|
|
27
|
-
- [Project Pastra](https://github.com/heiko-hotz/gemini-multimodal-live-dev-guide/tree/main): a comprehensive developer guide for the Gemini Multimodal Live API.
|
|
28
|
-
- [Google Cloud Multimodal Live API demos and samples](https://github.com/GoogleCloudPlatform/generative-ai/tree/main/gemini/multimodal-live-api): Collection of code samples and demo applications leveraging multimodal live API in Vertex AI
|
|
29
|
-
- [Gemini 2 Cookbook](https://github.com/google-gemini/cookbook/tree/main/gemini-2): Practical examples and tutorials for working with Gemini 2
|
|
30
|
-
- [Multimodal Live API Web Console](https://github.com/google-gemini/multimodal-live-api-web-console): Interactive React-based web interface for testing and experimenting with Gemini Multimodal Live API.
|
|
31
|
-
|
|
32
|
-
## Current Status & Future Work
|
|
33
|
-
|
|
34
|
-
This pattern is under active development. Key areas planned for future enhancement include:
|
|
35
|
-
|
|
36
|
-
* **Observability:** Implementing comprehensive monitoring and tracing features.
|
|
37
|
-
* **Load Testing:** Integrating load testing capabilities.
|
agents/live_api/app/agent.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
|
|
15
|
-
import os
|
|
16
|
-
|
|
17
|
-
import google.auth
|
|
18
|
-
import vertexai
|
|
19
|
-
from google import genai
|
|
20
|
-
from google.genai import types
|
|
21
|
-
|
|
22
|
-
# Constants
|
|
23
|
-
VERTEXAI = os.getenv("VERTEXAI", "true").lower() == "true"
|
|
24
|
-
LOCATION = "us-central1"
|
|
25
|
-
MODEL_ID = "gemini-live-2.5-flash-preview-native-audio"
|
|
26
|
-
|
|
27
|
-
# Initialize Google Cloud clients
|
|
28
|
-
credentials, project_id = google.auth.default()
|
|
29
|
-
vertexai.init(project=project_id, location=LOCATION)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if VERTEXAI:
|
|
33
|
-
genai_client = genai.Client(project=project_id, location=LOCATION, vertexai=True)
|
|
34
|
-
else:
|
|
35
|
-
# API key should be set using GOOGLE_API_KEY environment variable
|
|
36
|
-
genai_client = genai.Client(http_options={"api_version": "v1alpha"})
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_weather(query: str) -> dict:
|
|
40
|
-
"""Simulates a web search. Use it get information on weather.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
query: A string containing the location to get weather information for.
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
A string with the simulated weather information for the queried location.
|
|
47
|
-
"""
|
|
48
|
-
if "sf" in query.lower() or "san francisco" in query.lower():
|
|
49
|
-
return {"output": "It's 60 degrees and foggy."}
|
|
50
|
-
return {"output": "It's 90 degrees and sunny."}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Configure tools available to the agent and live connection
|
|
54
|
-
tool_functions = {"get_weather": get_weather}
|
|
55
|
-
|
|
56
|
-
live_connect_config = types.LiveConnectConfig(
|
|
57
|
-
response_modalities=[types.Modality.AUDIO],
|
|
58
|
-
tools=list(tool_functions.values()),
|
|
59
|
-
system_instruction=types.Content(
|
|
60
|
-
parts=[
|
|
61
|
-
types.Part(
|
|
62
|
-
text="""You are a helpful AI assistant designed to provide accurate and useful information. You are able to accommodate different languages and tones of voice."""
|
|
63
|
-
)
|
|
64
|
-
]
|
|
65
|
-
),
|
|
66
|
-
speech_config=types.SpeechConfig(
|
|
67
|
-
voice_config=types.VoiceConfig(
|
|
68
|
-
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore")
|
|
69
|
-
)
|
|
70
|
-
),
|
|
71
|
-
enable_affective_dialog=True,
|
|
72
|
-
)
|
|
@@ -1,260 +0,0 @@
|
|
|
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
|
-
|
|
15
|
-
import asyncio
|
|
16
|
-
import json
|
|
17
|
-
import logging
|
|
18
|
-
import os
|
|
19
|
-
import subprocess
|
|
20
|
-
import sys
|
|
21
|
-
import threading
|
|
22
|
-
import time
|
|
23
|
-
import uuid
|
|
24
|
-
from collections.abc import Iterator
|
|
25
|
-
from typing import Any
|
|
26
|
-
|
|
27
|
-
import pytest
|
|
28
|
-
import requests
|
|
29
|
-
import websockets.client
|
|
30
|
-
|
|
31
|
-
# Configure logging
|
|
32
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
33
|
-
logger = logging.getLogger(__name__)
|
|
34
|
-
|
|
35
|
-
BASE_URL = "ws://127.0.0.1:8000/"
|
|
36
|
-
WS_URL = BASE_URL + "ws"
|
|
37
|
-
|
|
38
|
-
FEEDBACK_URL = "http://127.0.0.1:8000/feedback"
|
|
39
|
-
HEADERS = {"Content-Type": "application/json"}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def log_output(pipe: Any, log_func: Any) -> None:
|
|
43
|
-
"""Log the output from the given pipe."""
|
|
44
|
-
for line in iter(pipe.readline, ""):
|
|
45
|
-
log_func(line.strip())
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def start_server() -> subprocess.Popen[str]:
|
|
49
|
-
"""Start the FastAPI server using subprocess and log its output."""
|
|
50
|
-
command = [
|
|
51
|
-
sys.executable,
|
|
52
|
-
"-m",
|
|
53
|
-
"uvicorn",
|
|
54
|
-
"app.server:app",
|
|
55
|
-
"--host",
|
|
56
|
-
"0.0.0.0",
|
|
57
|
-
"--port",
|
|
58
|
-
"8000",
|
|
59
|
-
]
|
|
60
|
-
env = os.environ.copy()
|
|
61
|
-
env["INTEGRATION_TEST"] = "TRUE"
|
|
62
|
-
process = subprocess.Popen(
|
|
63
|
-
command,
|
|
64
|
-
stdout=subprocess.PIPE,
|
|
65
|
-
stderr=subprocess.PIPE,
|
|
66
|
-
text=True,
|
|
67
|
-
bufsize=1,
|
|
68
|
-
env=env,
|
|
69
|
-
encoding="utf-8",
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# Start threads to log stdout and stderr in real-time
|
|
73
|
-
threading.Thread(
|
|
74
|
-
target=log_output, args=(process.stdout, logger.info), daemon=True
|
|
75
|
-
).start()
|
|
76
|
-
threading.Thread(
|
|
77
|
-
target=log_output, args=(process.stderr, logger.error), daemon=True
|
|
78
|
-
).start()
|
|
79
|
-
|
|
80
|
-
return process
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def wait_for_server(timeout: int = 60, interval: int = 1) -> bool:
|
|
84
|
-
"""Wait for the server to be ready."""
|
|
85
|
-
start_time = time.time()
|
|
86
|
-
while time.time() - start_time < timeout:
|
|
87
|
-
try:
|
|
88
|
-
response = requests.get("http://127.0.0.1:8000/docs", timeout=10)
|
|
89
|
-
if response.status_code == 200:
|
|
90
|
-
logger.info("Server is ready")
|
|
91
|
-
return True
|
|
92
|
-
except Exception:
|
|
93
|
-
pass
|
|
94
|
-
time.sleep(interval)
|
|
95
|
-
logger.error(f"Server did not become ready within {timeout} seconds")
|
|
96
|
-
return False
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@pytest.fixture(scope="session")
|
|
100
|
-
def server_fixture(request: Any) -> Iterator[subprocess.Popen[str]]:
|
|
101
|
-
"""Pytest fixture to start and stop the server for testing."""
|
|
102
|
-
logger.info("Starting server process")
|
|
103
|
-
server_process = start_server()
|
|
104
|
-
if not wait_for_server():
|
|
105
|
-
pytest.fail("Server failed to start")
|
|
106
|
-
logger.info("Server process started")
|
|
107
|
-
|
|
108
|
-
def stop_server() -> None:
|
|
109
|
-
logger.info("Stopping server process")
|
|
110
|
-
server_process.terminate()
|
|
111
|
-
try:
|
|
112
|
-
server_process.wait(timeout=5)
|
|
113
|
-
except subprocess.TimeoutExpired:
|
|
114
|
-
logger.warning("Server process did not terminate, killing it")
|
|
115
|
-
server_process.kill()
|
|
116
|
-
server_process.wait()
|
|
117
|
-
logger.info("Server process stopped")
|
|
118
|
-
|
|
119
|
-
request.addfinalizer(stop_server)
|
|
120
|
-
yield server_process
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@pytest.mark.asyncio
|
|
124
|
-
async def test_websocket_connection(server_fixture: subprocess.Popen[str]) -> None:
|
|
125
|
-
"""Test the websocket connection and message exchange."""
|
|
126
|
-
|
|
127
|
-
async def send_message(websocket: Any, message: dict[str, Any]) -> None:
|
|
128
|
-
"""Helper function to send messages and log them."""
|
|
129
|
-
await websocket.send(json.dumps(message))
|
|
130
|
-
|
|
131
|
-
async def receive_message(websocket: Any, timeout: float = 5.0) -> dict[str, Any]:
|
|
132
|
-
"""Helper function to receive messages with timeout."""
|
|
133
|
-
try:
|
|
134
|
-
response = await asyncio.wait_for(websocket.recv(), timeout=timeout)
|
|
135
|
-
if isinstance(response, bytes):
|
|
136
|
-
return json.loads(response.decode())
|
|
137
|
-
if isinstance(response, str):
|
|
138
|
-
return json.loads(response)
|
|
139
|
-
return response
|
|
140
|
-
except asyncio.TimeoutError as exc:
|
|
141
|
-
raise TimeoutError(
|
|
142
|
-
f"No response received within {timeout} seconds"
|
|
143
|
-
) from exc
|
|
144
|
-
|
|
145
|
-
try:
|
|
146
|
-
await asyncio.sleep(2)
|
|
147
|
-
|
|
148
|
-
async with websockets.connect(
|
|
149
|
-
WS_URL, ping_timeout=10, close_timeout=10
|
|
150
|
-
) as websocket:
|
|
151
|
-
try:
|
|
152
|
-
# Wait for initial ready message
|
|
153
|
-
initial_response = None
|
|
154
|
-
for _ in range(10):
|
|
155
|
-
try:
|
|
156
|
-
initial_response = await receive_message(websocket, timeout=5.0)
|
|
157
|
-
if (
|
|
158
|
-
initial_response is not None
|
|
159
|
-
and initial_response.get("status")
|
|
160
|
-
== "Backend is ready for conversation"
|
|
161
|
-
):
|
|
162
|
-
break
|
|
163
|
-
except TimeoutError:
|
|
164
|
-
if _ == 9:
|
|
165
|
-
raise
|
|
166
|
-
continue
|
|
167
|
-
|
|
168
|
-
assert (
|
|
169
|
-
initial_response is not None
|
|
170
|
-
and initial_response.get("status")
|
|
171
|
-
== "Backend is ready for conversation"
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
# Send messages
|
|
175
|
-
setup_msg = {"setup": {"run_id": "test-run", "user_id": "test-user"}}
|
|
176
|
-
await send_message(websocket, setup_msg)
|
|
177
|
-
|
|
178
|
-
dummy_audio = bytes([0] * 1024)
|
|
179
|
-
audio_msg = {
|
|
180
|
-
"realtimeInput": {
|
|
181
|
-
"mediaChunks": [
|
|
182
|
-
{
|
|
183
|
-
"mimeType": "audio/pcm;rate=16000",
|
|
184
|
-
"data": dummy_audio.hex(),
|
|
185
|
-
}
|
|
186
|
-
]
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
await send_message(websocket, audio_msg)
|
|
190
|
-
|
|
191
|
-
text_msg = {
|
|
192
|
-
"clientContent": {
|
|
193
|
-
"turns": [
|
|
194
|
-
{"role": "user", "parts": [{"text": "Hello, how are you?"}]}
|
|
195
|
-
],
|
|
196
|
-
"turnComplete": True,
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
await send_message(websocket, text_msg)
|
|
200
|
-
|
|
201
|
-
# Collect responses with timeout
|
|
202
|
-
responses = []
|
|
203
|
-
try:
|
|
204
|
-
while True:
|
|
205
|
-
try:
|
|
206
|
-
response = await receive_message(websocket, timeout=10.0)
|
|
207
|
-
responses.append(response)
|
|
208
|
-
if (
|
|
209
|
-
len(responses) >= 3
|
|
210
|
-
): # Exit after receiving enough responses
|
|
211
|
-
break
|
|
212
|
-
except TimeoutError:
|
|
213
|
-
break
|
|
214
|
-
except asyncio.TimeoutError:
|
|
215
|
-
logger.info("Response collection timed out")
|
|
216
|
-
|
|
217
|
-
# Verify responses
|
|
218
|
-
assert len(responses) > 0, "No responses received from server"
|
|
219
|
-
assert any(
|
|
220
|
-
isinstance(r, dict) and "serverContent" in r for r in responses
|
|
221
|
-
)
|
|
222
|
-
logger.info(
|
|
223
|
-
f"Test completed successfully. Received {len(responses)} responses"
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
finally:
|
|
227
|
-
# Ensure websocket is closed properly
|
|
228
|
-
await websocket.close()
|
|
229
|
-
|
|
230
|
-
except Exception as e:
|
|
231
|
-
logger.error(f"Test failed with error: {e!s}")
|
|
232
|
-
raise
|
|
233
|
-
|
|
234
|
-
finally:
|
|
235
|
-
# Clean up any remaining tasks
|
|
236
|
-
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
|
237
|
-
for task in tasks:
|
|
238
|
-
task.cancel()
|
|
239
|
-
if tasks:
|
|
240
|
-
await asyncio.gather(*tasks, return_exceptions=True)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
|
|
244
|
-
"""
|
|
245
|
-
Test the feedback collection endpoint (/feedback) to ensure it properly
|
|
246
|
-
logs the received feedback.
|
|
247
|
-
"""
|
|
248
|
-
# Create sample feedback data
|
|
249
|
-
feedback_data = {
|
|
250
|
-
"score": 4,
|
|
251
|
-
"text": "Great response!",
|
|
252
|
-
"run_id": str(uuid.uuid4()),
|
|
253
|
-
"user_id": "user1",
|
|
254
|
-
"log_type": "feedback",
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
response = requests.post(
|
|
258
|
-
FEEDBACK_URL, json=feedback_data, headers=HEADERS, timeout=10
|
|
259
|
-
)
|
|
260
|
-
assert response.status_code == 200
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
|
|
15
|
-
import time
|
|
16
|
-
|
|
17
|
-
from locust import HttpUser, between, task
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class DummyUser(HttpUser):
|
|
21
|
-
"""Simulates a user for testing purposes."""
|
|
22
|
-
|
|
23
|
-
wait_time = between(1, 3) # Wait 1-3 seconds between tasks
|
|
24
|
-
|
|
25
|
-
@task
|
|
26
|
-
def dummy_task(self) -> None:
|
|
27
|
-
"""A dummy task that simulates work without making actual requests."""
|
|
28
|
-
# Simulate some processing time
|
|
29
|
-
time.sleep(0.1)
|
|
30
|
-
|
|
31
|
-
# Record a successful dummy request
|
|
32
|
-
self.environment.events.request.fire(
|
|
33
|
-
request_type="POST",
|
|
34
|
-
name="dummy_endpoint",
|
|
35
|
-
response_time=100,
|
|
36
|
-
response_length=1024,
|
|
37
|
-
response=None,
|
|
38
|
-
context={},
|
|
39
|
-
exception=None,
|
|
40
|
-
)
|
|
@@ -1,144 +0,0 @@
|
|
|
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
|
-
|
|
15
|
-
import json
|
|
16
|
-
import logging
|
|
17
|
-
import os
|
|
18
|
-
from collections.abc import Generator
|
|
19
|
-
from unittest.mock import AsyncMock, MagicMock, patch
|
|
20
|
-
|
|
21
|
-
import pytest
|
|
22
|
-
from fastapi.testclient import TestClient
|
|
23
|
-
from google.auth.credentials import Credentials
|
|
24
|
-
|
|
25
|
-
# Set up logging
|
|
26
|
-
logging.basicConfig(level=logging.INFO)
|
|
27
|
-
logger = logging.getLogger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@pytest.fixture(autouse=True)
|
|
31
|
-
def mock_google_cloud_credentials() -> Generator[None, None, None]:
|
|
32
|
-
"""Mock Google Cloud credentials for testing."""
|
|
33
|
-
with patch.dict(
|
|
34
|
-
os.environ,
|
|
35
|
-
{
|
|
36
|
-
"GOOGLE_APPLICATION_CREDENTIALS": "/path/to/mock/credentials.json",
|
|
37
|
-
"GOOGLE_CLOUD_PROJECT_ID": "mock-project-id",
|
|
38
|
-
},
|
|
39
|
-
):
|
|
40
|
-
yield
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@pytest.fixture(autouse=True)
|
|
44
|
-
def mock_google_auth_default() -> Generator[None, None, None]:
|
|
45
|
-
"""Mock the google.auth.default function for testing."""
|
|
46
|
-
mock_credentials = MagicMock(spec=Credentials)
|
|
47
|
-
mock_project = "mock-project-id"
|
|
48
|
-
|
|
49
|
-
with patch("google.auth.default", return_value=(mock_credentials, mock_project)):
|
|
50
|
-
yield
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@pytest.fixture(autouse=True)
|
|
54
|
-
def mock_dependencies() -> Generator[None, None, None]:
|
|
55
|
-
"""
|
|
56
|
-
Mock Vertex AI dependencies for testing.
|
|
57
|
-
Patches genai client and tool functions.
|
|
58
|
-
"""
|
|
59
|
-
with (
|
|
60
|
-
patch("{{cookiecutter.agent_directory}}.server.genai_client") as mock_genai,
|
|
61
|
-
patch("{{cookiecutter.agent_directory}}.server.tool_functions") as mock_tools,
|
|
62
|
-
):
|
|
63
|
-
mock_genai.aio.live.connect = AsyncMock()
|
|
64
|
-
mock_tools.return_value = {}
|
|
65
|
-
yield
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@pytest.mark.asyncio
|
|
69
|
-
async def test_websocket_endpoint() -> None:
|
|
70
|
-
"""
|
|
71
|
-
Test the websocket endpoint to ensure it correctly handles
|
|
72
|
-
websocket connections and messages.
|
|
73
|
-
"""
|
|
74
|
-
from {{cookiecutter.agent_directory}}.server import app
|
|
75
|
-
|
|
76
|
-
mock_session = AsyncMock()
|
|
77
|
-
mock_session._ws = AsyncMock()
|
|
78
|
-
# Configure mock to return proper response format and close after one message
|
|
79
|
-
mock_session._ws.recv.side_effect = [
|
|
80
|
-
json.dumps(
|
|
81
|
-
{
|
|
82
|
-
"serverContent": {
|
|
83
|
-
"modelTurn": {
|
|
84
|
-
"role": "model",
|
|
85
|
-
"parts": [{"text": "Hello, how can I help you?"}],
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
).encode(), # Encode as bytes since recv(decode=False) is used
|
|
90
|
-
None, # Add None to trigger StopAsyncIteration after first message
|
|
91
|
-
]
|
|
92
|
-
|
|
93
|
-
with patch("{{cookiecutter.agent_directory}}.server.genai_client") as mock_genai:
|
|
94
|
-
mock_genai.aio.live.connect.return_value.__aenter__.return_value = mock_session
|
|
95
|
-
client = TestClient(app)
|
|
96
|
-
with client.websocket_connect("/ws") as websocket:
|
|
97
|
-
# Test initial connection message
|
|
98
|
-
data = websocket.receive_json()
|
|
99
|
-
assert data["status"] == "Backend is ready for conversation"
|
|
100
|
-
|
|
101
|
-
# Test sending a message
|
|
102
|
-
websocket.send_json(
|
|
103
|
-
{"setup": {"run_id": "test-run", "user_id": "test-user"}}
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Test sending audio stream
|
|
107
|
-
dummy_audio = bytes([0] * 1024) # 1KB of silence
|
|
108
|
-
websocket.send_json(
|
|
109
|
-
{
|
|
110
|
-
"realtimeInput": {
|
|
111
|
-
"mediaChunks": [
|
|
112
|
-
{
|
|
113
|
-
"mimeType": "audio/pcm;rate=16000",
|
|
114
|
-
"data": dummy_audio.hex(),
|
|
115
|
-
}
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
# Receive response as bytes
|
|
122
|
-
response = websocket.receive_bytes()
|
|
123
|
-
response_data = json.loads(response.decode())
|
|
124
|
-
assert "serverContent" in response_data
|
|
125
|
-
|
|
126
|
-
# Verify mock interactions
|
|
127
|
-
mock_genai.aio.live.connect.assert_called_once()
|
|
128
|
-
assert mock_session._ws.recv.called
|
|
129
|
-
await mock_session._ws.recv.aclose()
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
@pytest.mark.asyncio
|
|
133
|
-
def test_websocket_error_handling() -> None:
|
|
134
|
-
"""Test websocket error handling."""
|
|
135
|
-
from {{cookiecutter.agent_directory}}.server import app
|
|
136
|
-
|
|
137
|
-
with patch("{{cookiecutter.agent_directory}}.server.genai_client") as mock_genai:
|
|
138
|
-
mock_genai.aio.live.connect.side_effect = Exception("Connection failed")
|
|
139
|
-
|
|
140
|
-
client = TestClient(app)
|
|
141
|
-
with pytest.raises(Exception) as exc:
|
|
142
|
-
with client.websocket_connect("/ws"):
|
|
143
|
-
pass
|
|
144
|
-
assert str(exc.value) == "Connection failed"
|
|
File without changes
|
{agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/src/frontends/{live_api_react → adk_live_react}/frontend/src/components/audio-pulse/AudioPulse.tsx
RENAMED
|
File without changes
|
|
File without changes
|
/src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/mock-logs.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audioworklet-registry.ts
RENAMED
|
File without changes
|
|
File without changes
|
/src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/worklets/audio-processing.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|