agent-starter-pack 0.0.1b0__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.0.1b0.dist-info/METADATA +143 -0
- agent_starter_pack-0.0.1b0.dist-info/RECORD +162 -0
- agent_starter_pack-0.0.1b0.dist-info/WHEEL +4 -0
- agent_starter_pack-0.0.1b0.dist-info/entry_points.txt +2 -0
- agent_starter_pack-0.0.1b0.dist-info/licenses/LICENSE +201 -0
- agents/agentic_rag_vertexai_search/README.md +22 -0
- agents/agentic_rag_vertexai_search/app/agent.py +145 -0
- agents/agentic_rag_vertexai_search/app/retrievers.py +79 -0
- agents/agentic_rag_vertexai_search/app/templates.py +53 -0
- agents/agentic_rag_vertexai_search/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/agentic_rag_vertexai_search/template/.templateconfig.yaml +14 -0
- agents/agentic_rag_vertexai_search/tests/integration/test_agent.py +57 -0
- agents/crewai_coding_crew/README.md +34 -0
- agents/crewai_coding_crew/app/agent.py +86 -0
- agents/crewai_coding_crew/app/crew/config/agents.yaml +39 -0
- agents/crewai_coding_crew/app/crew/config/tasks.yaml +37 -0
- agents/crewai_coding_crew/app/crew/crew.py +71 -0
- agents/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +1571 -0
- agents/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/crewai_coding_crew/template/.templateconfig.yaml +12 -0
- agents/crewai_coding_crew/tests/integration/test_agent.py +47 -0
- agents/langgraph_base_react/README.md +9 -0
- agents/langgraph_base_react/app/agent.py +73 -0
- agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/langgraph_base_react/template/.templateconfig.yaml +13 -0
- agents/langgraph_base_react/tests/integration/test_agent.py +48 -0
- agents/multimodal_live_api/README.md +50 -0
- agents/multimodal_live_api/app/agent.py +86 -0
- agents/multimodal_live_api/app/server.py +193 -0
- agents/multimodal_live_api/app/templates.py +51 -0
- agents/multimodal_live_api/app/vector_store.py +55 -0
- agents/multimodal_live_api/template/.templateconfig.yaml +15 -0
- agents/multimodal_live_api/tests/integration/test_server_e2e.py +254 -0
- agents/multimodal_live_api/tests/load_test/load_test.py +40 -0
- agents/multimodal_live_api/tests/unit/test_server.py +143 -0
- src/base_template/.gitignore +197 -0
- src/base_template/Makefile +37 -0
- src/base_template/README.md +91 -0
- src/base_template/app/utils/tracing.py +143 -0
- src/base_template/app/utils/typing.py +115 -0
- src/base_template/deployment/README.md +123 -0
- src/base_template/deployment/cd/deploy-to-prod.yaml +98 -0
- src/base_template/deployment/cd/staging.yaml +215 -0
- src/base_template/deployment/ci/pr_checks.yaml +51 -0
- src/base_template/deployment/terraform/apis.tf +34 -0
- src/base_template/deployment/terraform/build_triggers.tf +122 -0
- src/base_template/deployment/terraform/dev/apis.tf +42 -0
- src/base_template/deployment/terraform/dev/iam.tf +90 -0
- src/base_template/deployment/terraform/dev/log_sinks.tf +66 -0
- src/base_template/deployment/terraform/dev/providers.tf +29 -0
- src/base_template/deployment/terraform/dev/storage.tf +76 -0
- src/base_template/deployment/terraform/dev/variables.tf +126 -0
- src/base_template/deployment/terraform/dev/vars/env.tfvars +21 -0
- src/base_template/deployment/terraform/iam.tf +130 -0
- src/base_template/deployment/terraform/locals.tf +50 -0
- src/base_template/deployment/terraform/log_sinks.tf +72 -0
- src/base_template/deployment/terraform/providers.tf +35 -0
- src/base_template/deployment/terraform/service_accounts.tf +42 -0
- src/base_template/deployment/terraform/storage.tf +100 -0
- src/base_template/deployment/terraform/variables.tf +202 -0
- src/base_template/deployment/terraform/vars/env.tfvars +43 -0
- src/base_template/pyproject.toml +113 -0
- src/base_template/tests/unit/test_utils/test_tracing_exporter.py +140 -0
- src/cli/commands/create.py +534 -0
- src/cli/commands/setup_cicd.py +730 -0
- src/cli/main.py +35 -0
- src/cli/utils/__init__.py +35 -0
- src/cli/utils/cicd.py +662 -0
- src/cli/utils/gcp.py +120 -0
- src/cli/utils/logging.py +51 -0
- src/cli/utils/template.py +644 -0
- src/data_ingestion/README.md +79 -0
- src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +175 -0
- src/data_ingestion/data_ingestion_pipeline/components/process_data.py +321 -0
- src/data_ingestion/data_ingestion_pipeline/pipeline.py +58 -0
- src/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +184 -0
- src/data_ingestion/pyproject.toml +17 -0
- src/data_ingestion/uv.lock +999 -0
- src/deployment_targets/agent_engine/app/agent_engine_app.py +238 -0
- src/deployment_targets/agent_engine/app/utils/gcs.py +42 -0
- src/deployment_targets/agent_engine/deployment_metadata.json +4 -0
- src/deployment_targets/agent_engine/notebooks/intro_reasoning_engine.ipynb +869 -0
- src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +120 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/report.html +264 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_exceptions.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_failures.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats.csv +3 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats_history.csv +22 -0
- src/deployment_targets/agent_engine/tests/load_test/README.md +42 -0
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +100 -0
- src/deployment_targets/agent_engine/tests/unit/test_dummy.py +22 -0
- src/deployment_targets/cloud_run/Dockerfile +29 -0
- src/deployment_targets/cloud_run/app/server.py +128 -0
- src/deployment_targets/cloud_run/deployment/terraform/artifact_registry.tf +22 -0
- src/deployment_targets/cloud_run/deployment/terraform/dev/service_accounts.tf +20 -0
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +192 -0
- src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/cloud_run/tests/load_test/README.md +79 -0
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +85 -0
- src/deployment_targets/cloud_run/tests/unit/test_server.py +142 -0
- src/deployment_targets/cloud_run/uv.lock +6952 -0
- src/frontends/live_api_react/frontend/package-lock.json +19405 -0
- src/frontends/live_api_react/frontend/package.json +56 -0
- src/frontends/live_api_react/frontend/public/favicon.ico +0 -0
- src/frontends/live_api_react/frontend/public/index.html +62 -0
- src/frontends/live_api_react/frontend/public/robots.txt +3 -0
- src/frontends/live_api_react/frontend/src/App.scss +189 -0
- src/frontends/live_api_react/frontend/src/App.test.tsx +25 -0
- src/frontends/live_api_react/frontend/src/App.tsx +205 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/AudioPulse.tsx +64 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/audio-pulse.scss +68 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +217 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +201 -0
- src/frontends/live_api_react/frontend/src/components/logger/Logger.tsx +241 -0
- src/frontends/live_api_react/frontend/src/components/logger/logger.scss +133 -0
- src/frontends/live_api_react/frontend/src/components/logger/mock-logs.ts +151 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +161 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +285 -0
- src/frontends/live_api_react/frontend/src/contexts/LiveAPIContext.tsx +48 -0
- src/frontends/live_api_react/frontend/src/hooks/use-live-api.ts +115 -0
- src/frontends/live_api_react/frontend/src/hooks/use-media-stream-mux.ts +23 -0
- src/frontends/live_api_react/frontend/src/hooks/use-screen-capture.ts +72 -0
- src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts +69 -0
- src/frontends/live_api_react/frontend/src/index.css +28 -0
- src/frontends/live_api_react/frontend/src/index.tsx +35 -0
- src/frontends/live_api_react/frontend/src/multimodal-live-types.ts +242 -0
- src/frontends/live_api_react/frontend/src/react-app-env.d.ts +17 -0
- src/frontends/live_api_react/frontend/src/reportWebVitals.ts +31 -0
- src/frontends/live_api_react/frontend/src/setupTests.ts +21 -0
- src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts +111 -0
- src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts +270 -0
- src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts +43 -0
- src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +329 -0
- src/frontends/live_api_react/frontend/src/utils/store-logger.ts +64 -0
- src/frontends/live_api_react/frontend/src/utils/utils.ts +86 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts +73 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/vol-meter.ts +65 -0
- src/frontends/live_api_react/frontend/tsconfig.json +25 -0
- src/frontends/streamlit/frontend/side_bar.py +213 -0
- src/frontends/streamlit/frontend/streamlit_app.py +263 -0
- src/frontends/streamlit/frontend/style/app_markdown.py +37 -0
- src/frontends/streamlit/frontend/utils/chat_utils.py +67 -0
- src/frontends/streamlit/frontend/utils/local_chat_history.py +125 -0
- src/frontends/streamlit/frontend/utils/message_editing.py +59 -0
- src/frontends/streamlit/frontend/utils/multimodal_utils.py +217 -0
- src/frontends/streamlit/frontend/utils/stream_handler.py +282 -0
- src/frontends/streamlit/frontend/utils/title_summary.py +77 -0
- src/resources/containers/data_processing/Dockerfile +25 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-agent_engine.lock +4684 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-cloud_run.lock +5799 -0
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +5509 -0
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +6688 -0
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +4595 -0
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +5710 -0
- src/resources/locks/uv-multimodal_live_api-cloud_run.lock +5665 -0
- src/resources/setup_cicd/cicd_variables.tf +36 -0
- src/resources/setup_cicd/github.tf +85 -0
- src/resources/setup_cicd/providers.tf +39 -0
- src/utils/generate_locks.py +135 -0
- src/utils/lock_utils.py +82 -0
- src/utils/watch_and_rebuild.py +190 -0
|
@@ -0,0 +1,254 @@
|
|
|
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
|
+
)
|
|
70
|
+
|
|
71
|
+
# Start threads to log stdout and stderr in real-time
|
|
72
|
+
threading.Thread(
|
|
73
|
+
target=log_output, args=(process.stdout, logger.info), daemon=True
|
|
74
|
+
).start()
|
|
75
|
+
threading.Thread(
|
|
76
|
+
target=log_output, args=(process.stderr, logger.error), daemon=True
|
|
77
|
+
).start()
|
|
78
|
+
|
|
79
|
+
return process
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def wait_for_server(timeout: int = 60, interval: int = 1) -> bool:
|
|
83
|
+
"""Wait for the server to be ready."""
|
|
84
|
+
start_time = time.time()
|
|
85
|
+
while time.time() - start_time < timeout:
|
|
86
|
+
try:
|
|
87
|
+
response = requests.get("http://127.0.0.1:8000/docs", timeout=10)
|
|
88
|
+
if response.status_code == 200:
|
|
89
|
+
logger.info("Server is ready")
|
|
90
|
+
return True
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
time.sleep(interval)
|
|
94
|
+
logger.error(f"Server did not become ready within {timeout} seconds")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.fixture(scope="session")
|
|
99
|
+
def server_fixture(request: Any) -> Iterator[subprocess.Popen[str]]:
|
|
100
|
+
"""Pytest fixture to start and stop the server for testing."""
|
|
101
|
+
logger.info("Starting server process")
|
|
102
|
+
server_process = start_server()
|
|
103
|
+
if not wait_for_server():
|
|
104
|
+
pytest.fail("Server failed to start")
|
|
105
|
+
logger.info("Server process started")
|
|
106
|
+
|
|
107
|
+
def stop_server() -> None:
|
|
108
|
+
logger.info("Stopping server process")
|
|
109
|
+
server_process.terminate()
|
|
110
|
+
server_process.wait()
|
|
111
|
+
logger.info("Server process stopped")
|
|
112
|
+
|
|
113
|
+
request.addfinalizer(stop_server)
|
|
114
|
+
yield server_process
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_websocket_connection(server_fixture: subprocess.Popen[str]) -> None:
|
|
119
|
+
"""Test the websocket connection and message exchange."""
|
|
120
|
+
|
|
121
|
+
async def send_message(websocket: Any, message: dict[str, Any]) -> None:
|
|
122
|
+
"""Helper function to send messages and log them."""
|
|
123
|
+
await websocket.send(json.dumps(message))
|
|
124
|
+
|
|
125
|
+
async def receive_message(websocket: Any, timeout: float = 5.0) -> dict[str, Any]:
|
|
126
|
+
"""Helper function to receive messages with timeout."""
|
|
127
|
+
try:
|
|
128
|
+
response = await asyncio.wait_for(websocket.recv(), timeout=timeout)
|
|
129
|
+
if isinstance(response, bytes):
|
|
130
|
+
return json.loads(response.decode())
|
|
131
|
+
if isinstance(response, str):
|
|
132
|
+
return json.loads(response)
|
|
133
|
+
return response
|
|
134
|
+
except asyncio.TimeoutError as exc:
|
|
135
|
+
raise TimeoutError(
|
|
136
|
+
f"No response received within {timeout} seconds"
|
|
137
|
+
) from exc
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
await asyncio.sleep(2)
|
|
141
|
+
|
|
142
|
+
async with websockets.connect(
|
|
143
|
+
WS_URL, ping_timeout=10, close_timeout=10
|
|
144
|
+
) as websocket:
|
|
145
|
+
try:
|
|
146
|
+
# Wait for initial ready message
|
|
147
|
+
initial_response = None
|
|
148
|
+
for _ in range(10):
|
|
149
|
+
try:
|
|
150
|
+
initial_response = await receive_message(websocket, timeout=5.0)
|
|
151
|
+
if (
|
|
152
|
+
initial_response is not None
|
|
153
|
+
and initial_response.get("status")
|
|
154
|
+
== "Backend is ready for conversation"
|
|
155
|
+
):
|
|
156
|
+
break
|
|
157
|
+
except TimeoutError:
|
|
158
|
+
if _ == 9:
|
|
159
|
+
raise
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
assert (
|
|
163
|
+
initial_response is not None
|
|
164
|
+
and initial_response.get("status")
|
|
165
|
+
== "Backend is ready for conversation"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Send messages
|
|
169
|
+
setup_msg = {"setup": {"run_id": "test-run", "user_id": "test-user"}}
|
|
170
|
+
await send_message(websocket, setup_msg)
|
|
171
|
+
|
|
172
|
+
dummy_audio = bytes([0] * 1024)
|
|
173
|
+
audio_msg = {
|
|
174
|
+
"realtimeInput": {
|
|
175
|
+
"mediaChunks": [
|
|
176
|
+
{
|
|
177
|
+
"mimeType": "audio/pcm;rate=16000",
|
|
178
|
+
"data": dummy_audio.hex(),
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await send_message(websocket, audio_msg)
|
|
184
|
+
|
|
185
|
+
text_msg = {
|
|
186
|
+
"clientContent": {
|
|
187
|
+
"turns": [
|
|
188
|
+
{"role": "user", "parts": [{"text": "Hello, how are you?"}]}
|
|
189
|
+
],
|
|
190
|
+
"turnComplete": True,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await send_message(websocket, text_msg)
|
|
194
|
+
|
|
195
|
+
# Collect responses with timeout
|
|
196
|
+
responses = []
|
|
197
|
+
try:
|
|
198
|
+
while True:
|
|
199
|
+
try:
|
|
200
|
+
response = await receive_message(websocket, timeout=10.0)
|
|
201
|
+
responses.append(response)
|
|
202
|
+
if (
|
|
203
|
+
len(responses) >= 3
|
|
204
|
+
): # Exit after receiving enough responses
|
|
205
|
+
break
|
|
206
|
+
except TimeoutError:
|
|
207
|
+
break
|
|
208
|
+
except asyncio.TimeoutError:
|
|
209
|
+
logger.info("Response collection timed out")
|
|
210
|
+
|
|
211
|
+
# Verify responses
|
|
212
|
+
assert len(responses) > 0, "No responses received from server"
|
|
213
|
+
assert any(
|
|
214
|
+
isinstance(r, dict) and "serverContent" in r for r in responses
|
|
215
|
+
)
|
|
216
|
+
logger.info(
|
|
217
|
+
f"Test completed successfully. Received {len(responses)} responses"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
finally:
|
|
221
|
+
# Ensure websocket is closed properly
|
|
222
|
+
await websocket.close()
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"Test failed with error: {e!s}")
|
|
226
|
+
raise
|
|
227
|
+
|
|
228
|
+
finally:
|
|
229
|
+
# Clean up any remaining tasks
|
|
230
|
+
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
|
231
|
+
for task in tasks:
|
|
232
|
+
task.cancel()
|
|
233
|
+
if tasks:
|
|
234
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Test the feedback collection endpoint (/feedback) to ensure it properly
|
|
240
|
+
logs the received feedback.
|
|
241
|
+
"""
|
|
242
|
+
# Create sample feedback data
|
|
243
|
+
feedback_data = {
|
|
244
|
+
"score": 4,
|
|
245
|
+
"text": "Great response!",
|
|
246
|
+
"run_id": str(uuid.uuid4()),
|
|
247
|
+
"user_id": "user1",
|
|
248
|
+
"log_type": "feedback",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
response = requests.post(
|
|
252
|
+
FEEDBACK_URL, json=feedback_data, headers=HEADERS, timeout=10
|
|
253
|
+
)
|
|
254
|
+
assert response.status_code == 200
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
)
|
|
@@ -0,0 +1,143 @@
|
|
|
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("app.server.genai_client") as mock_genai,
|
|
61
|
+
patch("app.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 app.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("app.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
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.asyncio
|
|
132
|
+
async def test_websocket_error_handling() -> None:
|
|
133
|
+
"""Test websocket error handling."""
|
|
134
|
+
from app.server import app
|
|
135
|
+
|
|
136
|
+
with patch("app.server.genai_client") as mock_genai:
|
|
137
|
+
mock_genai.aio.live.connect.side_effect = Exception("Connection failed")
|
|
138
|
+
|
|
139
|
+
client = TestClient(app)
|
|
140
|
+
with pytest.raises(Exception) as exc:
|
|
141
|
+
with client.websocket_connect("/ws"):
|
|
142
|
+
pass
|
|
143
|
+
assert str(exc.value) == "Connection failed"
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyc
|
|
5
|
+
*$py.class
|
|
6
|
+
**/dist
|
|
7
|
+
/tmp
|
|
8
|
+
/out-tsc
|
|
9
|
+
/bazel-out
|
|
10
|
+
|
|
11
|
+
# C extensions
|
|
12
|
+
*.so
|
|
13
|
+
|
|
14
|
+
# Distribution / packaging
|
|
15
|
+
.Python
|
|
16
|
+
build/
|
|
17
|
+
develop-eggs/
|
|
18
|
+
dist/
|
|
19
|
+
downloads/
|
|
20
|
+
eggs/
|
|
21
|
+
.eggs/
|
|
22
|
+
lib/
|
|
23
|
+
lib64/
|
|
24
|
+
parts/
|
|
25
|
+
sdist/
|
|
26
|
+
var/
|
|
27
|
+
wheels/
|
|
28
|
+
pip-wheel-metadata/
|
|
29
|
+
share/python-wheels/
|
|
30
|
+
*.egg-info/
|
|
31
|
+
.installed.cfg
|
|
32
|
+
*.egg
|
|
33
|
+
MANIFEST
|
|
34
|
+
|
|
35
|
+
# PyInstaller
|
|
36
|
+
# Usually these files are written by a python script from a template
|
|
37
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
38
|
+
*.manifest
|
|
39
|
+
*.spec
|
|
40
|
+
|
|
41
|
+
# Installer logs
|
|
42
|
+
pip-log.txt
|
|
43
|
+
pip-delete-this-directory.txt
|
|
44
|
+
|
|
45
|
+
# Unit test / coverage reports
|
|
46
|
+
htmlcov/
|
|
47
|
+
.tox/
|
|
48
|
+
.nox/
|
|
49
|
+
.coverage
|
|
50
|
+
.coverage.*
|
|
51
|
+
.cache
|
|
52
|
+
nosetests.xml
|
|
53
|
+
coverage.xml
|
|
54
|
+
*.cover
|
|
55
|
+
*.py,cover
|
|
56
|
+
.hypothesis/
|
|
57
|
+
.pytest_cache/
|
|
58
|
+
|
|
59
|
+
# Translations
|
|
60
|
+
*.mo
|
|
61
|
+
*.pot
|
|
62
|
+
|
|
63
|
+
# Django stuff:
|
|
64
|
+
*.log
|
|
65
|
+
local_settings.py
|
|
66
|
+
db.sqlite3
|
|
67
|
+
db.sqlite3-journal
|
|
68
|
+
|
|
69
|
+
# Flask stuff:
|
|
70
|
+
instance/
|
|
71
|
+
.webassets-cache
|
|
72
|
+
|
|
73
|
+
# Scrapy stuff:
|
|
74
|
+
.scrapy
|
|
75
|
+
|
|
76
|
+
# Sphinx documentation
|
|
77
|
+
docs/_build/
|
|
78
|
+
|
|
79
|
+
# PyBuilder
|
|
80
|
+
target/
|
|
81
|
+
|
|
82
|
+
# Jupyter Notebook
|
|
83
|
+
.ipynb_checkpoints
|
|
84
|
+
|
|
85
|
+
# IPython
|
|
86
|
+
profile_default/
|
|
87
|
+
ipython_config.py
|
|
88
|
+
|
|
89
|
+
# pyenv
|
|
90
|
+
.python-version
|
|
91
|
+
|
|
92
|
+
# pipenv
|
|
93
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
94
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
95
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
96
|
+
# install all needed dependencies.
|
|
97
|
+
Pipfile.lock
|
|
98
|
+
Pipfile
|
|
99
|
+
|
|
100
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
101
|
+
__pypackages__/
|
|
102
|
+
|
|
103
|
+
# Celery stuff
|
|
104
|
+
celerybeat-schedule
|
|
105
|
+
celerybeat.pid
|
|
106
|
+
|
|
107
|
+
# SageMath parsed files
|
|
108
|
+
*.sage.py
|
|
109
|
+
|
|
110
|
+
# Environments
|
|
111
|
+
.env
|
|
112
|
+
.venv
|
|
113
|
+
.venv*
|
|
114
|
+
env/
|
|
115
|
+
venv/
|
|
116
|
+
ENV/
|
|
117
|
+
env.bak/
|
|
118
|
+
venv.bak/
|
|
119
|
+
|
|
120
|
+
# Spyder project settings
|
|
121
|
+
.spyderproject
|
|
122
|
+
.spyproject
|
|
123
|
+
|
|
124
|
+
# Rope project settings
|
|
125
|
+
.ropeproject
|
|
126
|
+
|
|
127
|
+
# mkdocs documentation
|
|
128
|
+
/site
|
|
129
|
+
|
|
130
|
+
# mypy
|
|
131
|
+
.mypy_cache/
|
|
132
|
+
.dmypy.json
|
|
133
|
+
dmypy.json
|
|
134
|
+
|
|
135
|
+
# Pyre type checker
|
|
136
|
+
.pyre/
|
|
137
|
+
|
|
138
|
+
# macOS
|
|
139
|
+
.DS_Store
|
|
140
|
+
|
|
141
|
+
# PyCharm
|
|
142
|
+
.idea
|
|
143
|
+
|
|
144
|
+
# User-specific files
|
|
145
|
+
.terraform*
|
|
146
|
+
.Terraform*
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
tmp*
|
|
150
|
+
|
|
151
|
+
# Node
|
|
152
|
+
**/node_modules
|
|
153
|
+
npm-debug.log
|
|
154
|
+
yarn-error.log
|
|
155
|
+
|
|
156
|
+
# IDEs and editors
|
|
157
|
+
.idea/
|
|
158
|
+
.project
|
|
159
|
+
.classpath
|
|
160
|
+
.c9/
|
|
161
|
+
*.launch
|
|
162
|
+
.settings/
|
|
163
|
+
*.sublime-workspace
|
|
164
|
+
|
|
165
|
+
# Visual Studio Code
|
|
166
|
+
.vscode/*
|
|
167
|
+
!.vscode/settings.json
|
|
168
|
+
!.vscode/tasks.json
|
|
169
|
+
!.vscode/launch.json
|
|
170
|
+
!.vscode/extensions.json
|
|
171
|
+
.history/*
|
|
172
|
+
|
|
173
|
+
# Miscellaneous
|
|
174
|
+
**/.angular/*
|
|
175
|
+
/.angular/cache
|
|
176
|
+
.sass-cache/
|
|
177
|
+
/connect.lock
|
|
178
|
+
/coverage
|
|
179
|
+
/libpeerconnection.log
|
|
180
|
+
testem.log
|
|
181
|
+
/typings
|
|
182
|
+
|
|
183
|
+
# System files
|
|
184
|
+
.DS_Store
|
|
185
|
+
Thumbs.db
|
|
186
|
+
*.vscode*
|
|
187
|
+
|
|
188
|
+
# Starter pack specific
|
|
189
|
+
.persist_vector_store
|
|
190
|
+
tests/load_test/.results/*.html
|
|
191
|
+
tests/load_test/.results/*.csv
|
|
192
|
+
locust_env
|
|
193
|
+
my_env.tfvars
|
|
194
|
+
.streamlit_chats
|
|
195
|
+
.saved_chats
|
|
196
|
+
.env
|
|
197
|
+
.requirements.txt
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
test:
|
|
2
|
+
uv run pytest tests/unit && uv run pytest tests/integration
|
|
3
|
+
|
|
4
|
+
install:
|
|
5
|
+
uv sync --dev {% if cookiecutter.agent_name != 'multimodal_live_api' %}--extra streamlit{%- endif %} --extra jupyter --frozen{% if cookiecutter.agent_name == 'multimodal_live_api' %} && npm --prefix frontend install{%- endif %}
|
|
6
|
+
|
|
7
|
+
playground:
|
|
8
|
+
{%- if cookiecutter.deployment_target == 'cloud_run' %}
|
|
9
|
+
uv run uvicorn app.server:app --host 0.0.0.0 --port 8000 --reload &
|
|
10
|
+
{%- endif %}
|
|
11
|
+
{%- if cookiecutter.agent_name == 'multimodal_live_api' %}
|
|
12
|
+
npm --prefix frontend start
|
|
13
|
+
{%- else %}
|
|
14
|
+
{% if cookiecutter.deployment_target == 'agent_engine' %}PYTHONPATH=. {% endif %}uv run streamlit run frontend/streamlit_app.py --browser.serverAddress=localhost --server.enableCORS=false --server.enableXsrfProtection=false
|
|
15
|
+
{%- endif %}
|
|
16
|
+
|
|
17
|
+
backend:
|
|
18
|
+
{%- if cookiecutter.deployment_target == 'cloud_run' %}
|
|
19
|
+
uv run uvicorn app.server:app --host 0.0.0.0 --port 8000 --reload
|
|
20
|
+
{%- elif cookiecutter.deployment_target == 'agent_engine' %}
|
|
21
|
+
uv export --no-hashes --no-sources --no-header --no-emit-project --frozen > .requirements.txt && uv run app/agent_engine_app.py
|
|
22
|
+
{%- endif %}
|
|
23
|
+
|
|
24
|
+
{% if cookiecutter.deployment_target == 'cloud_run' -%}
|
|
25
|
+
ui:
|
|
26
|
+
{%- if cookiecutter.agent_name == 'multimodal_live_api' %}
|
|
27
|
+
npm --prefix frontend start
|
|
28
|
+
{%- else %}
|
|
29
|
+
uv run streamlit run streamlit/streamlit_app.py --browser.serverAddress=localhost --server.enableCORS=false --server.enableXsrfProtection=false
|
|
30
|
+
{%- endif %}
|
|
31
|
+
{%- endif %}
|
|
32
|
+
|
|
33
|
+
lint:
|
|
34
|
+
uv run codespell
|
|
35
|
+
uv run ruff check . --diff
|
|
36
|
+
uv run ruff format . --check --diff
|
|
37
|
+
uv run mypy .
|