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.

Files changed (162) hide show
  1. agent_starter_pack-0.0.1b0.dist-info/METADATA +143 -0
  2. agent_starter_pack-0.0.1b0.dist-info/RECORD +162 -0
  3. agent_starter_pack-0.0.1b0.dist-info/WHEEL +4 -0
  4. agent_starter_pack-0.0.1b0.dist-info/entry_points.txt +2 -0
  5. agent_starter_pack-0.0.1b0.dist-info/licenses/LICENSE +201 -0
  6. agents/agentic_rag_vertexai_search/README.md +22 -0
  7. agents/agentic_rag_vertexai_search/app/agent.py +145 -0
  8. agents/agentic_rag_vertexai_search/app/retrievers.py +79 -0
  9. agents/agentic_rag_vertexai_search/app/templates.py +53 -0
  10. agents/agentic_rag_vertexai_search/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  11. agents/agentic_rag_vertexai_search/template/.templateconfig.yaml +14 -0
  12. agents/agentic_rag_vertexai_search/tests/integration/test_agent.py +57 -0
  13. agents/crewai_coding_crew/README.md +34 -0
  14. agents/crewai_coding_crew/app/agent.py +86 -0
  15. agents/crewai_coding_crew/app/crew/config/agents.yaml +39 -0
  16. agents/crewai_coding_crew/app/crew/config/tasks.yaml +37 -0
  17. agents/crewai_coding_crew/app/crew/crew.py +71 -0
  18. agents/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +1571 -0
  19. agents/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  20. agents/crewai_coding_crew/template/.templateconfig.yaml +12 -0
  21. agents/crewai_coding_crew/tests/integration/test_agent.py +47 -0
  22. agents/langgraph_base_react/README.md +9 -0
  23. agents/langgraph_base_react/app/agent.py +73 -0
  24. agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  25. agents/langgraph_base_react/template/.templateconfig.yaml +13 -0
  26. agents/langgraph_base_react/tests/integration/test_agent.py +48 -0
  27. agents/multimodal_live_api/README.md +50 -0
  28. agents/multimodal_live_api/app/agent.py +86 -0
  29. agents/multimodal_live_api/app/server.py +193 -0
  30. agents/multimodal_live_api/app/templates.py +51 -0
  31. agents/multimodal_live_api/app/vector_store.py +55 -0
  32. agents/multimodal_live_api/template/.templateconfig.yaml +15 -0
  33. agents/multimodal_live_api/tests/integration/test_server_e2e.py +254 -0
  34. agents/multimodal_live_api/tests/load_test/load_test.py +40 -0
  35. agents/multimodal_live_api/tests/unit/test_server.py +143 -0
  36. src/base_template/.gitignore +197 -0
  37. src/base_template/Makefile +37 -0
  38. src/base_template/README.md +91 -0
  39. src/base_template/app/utils/tracing.py +143 -0
  40. src/base_template/app/utils/typing.py +115 -0
  41. src/base_template/deployment/README.md +123 -0
  42. src/base_template/deployment/cd/deploy-to-prod.yaml +98 -0
  43. src/base_template/deployment/cd/staging.yaml +215 -0
  44. src/base_template/deployment/ci/pr_checks.yaml +51 -0
  45. src/base_template/deployment/terraform/apis.tf +34 -0
  46. src/base_template/deployment/terraform/build_triggers.tf +122 -0
  47. src/base_template/deployment/terraform/dev/apis.tf +42 -0
  48. src/base_template/deployment/terraform/dev/iam.tf +90 -0
  49. src/base_template/deployment/terraform/dev/log_sinks.tf +66 -0
  50. src/base_template/deployment/terraform/dev/providers.tf +29 -0
  51. src/base_template/deployment/terraform/dev/storage.tf +76 -0
  52. src/base_template/deployment/terraform/dev/variables.tf +126 -0
  53. src/base_template/deployment/terraform/dev/vars/env.tfvars +21 -0
  54. src/base_template/deployment/terraform/iam.tf +130 -0
  55. src/base_template/deployment/terraform/locals.tf +50 -0
  56. src/base_template/deployment/terraform/log_sinks.tf +72 -0
  57. src/base_template/deployment/terraform/providers.tf +35 -0
  58. src/base_template/deployment/terraform/service_accounts.tf +42 -0
  59. src/base_template/deployment/terraform/storage.tf +100 -0
  60. src/base_template/deployment/terraform/variables.tf +202 -0
  61. src/base_template/deployment/terraform/vars/env.tfvars +43 -0
  62. src/base_template/pyproject.toml +113 -0
  63. src/base_template/tests/unit/test_utils/test_tracing_exporter.py +140 -0
  64. src/cli/commands/create.py +534 -0
  65. src/cli/commands/setup_cicd.py +730 -0
  66. src/cli/main.py +35 -0
  67. src/cli/utils/__init__.py +35 -0
  68. src/cli/utils/cicd.py +662 -0
  69. src/cli/utils/gcp.py +120 -0
  70. src/cli/utils/logging.py +51 -0
  71. src/cli/utils/template.py +644 -0
  72. src/data_ingestion/README.md +79 -0
  73. src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +175 -0
  74. src/data_ingestion/data_ingestion_pipeline/components/process_data.py +321 -0
  75. src/data_ingestion/data_ingestion_pipeline/pipeline.py +58 -0
  76. src/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +184 -0
  77. src/data_ingestion/pyproject.toml +17 -0
  78. src/data_ingestion/uv.lock +999 -0
  79. src/deployment_targets/agent_engine/app/agent_engine_app.py +238 -0
  80. src/deployment_targets/agent_engine/app/utils/gcs.py +42 -0
  81. src/deployment_targets/agent_engine/deployment_metadata.json +4 -0
  82. src/deployment_targets/agent_engine/notebooks/intro_reasoning_engine.ipynb +869 -0
  83. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +120 -0
  84. src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
  85. src/deployment_targets/agent_engine/tests/load_test/.results/report.html +264 -0
  86. src/deployment_targets/agent_engine/tests/load_test/.results/results_exceptions.csv +1 -0
  87. src/deployment_targets/agent_engine/tests/load_test/.results/results_failures.csv +1 -0
  88. src/deployment_targets/agent_engine/tests/load_test/.results/results_stats.csv +3 -0
  89. src/deployment_targets/agent_engine/tests/load_test/.results/results_stats_history.csv +22 -0
  90. src/deployment_targets/agent_engine/tests/load_test/README.md +42 -0
  91. src/deployment_targets/agent_engine/tests/load_test/load_test.py +100 -0
  92. src/deployment_targets/agent_engine/tests/unit/test_dummy.py +22 -0
  93. src/deployment_targets/cloud_run/Dockerfile +29 -0
  94. src/deployment_targets/cloud_run/app/server.py +128 -0
  95. src/deployment_targets/cloud_run/deployment/terraform/artifact_registry.tf +22 -0
  96. src/deployment_targets/cloud_run/deployment/terraform/dev/service_accounts.tf +20 -0
  97. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +192 -0
  98. src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
  99. src/deployment_targets/cloud_run/tests/load_test/README.md +79 -0
  100. src/deployment_targets/cloud_run/tests/load_test/load_test.py +85 -0
  101. src/deployment_targets/cloud_run/tests/unit/test_server.py +142 -0
  102. src/deployment_targets/cloud_run/uv.lock +6952 -0
  103. src/frontends/live_api_react/frontend/package-lock.json +19405 -0
  104. src/frontends/live_api_react/frontend/package.json +56 -0
  105. src/frontends/live_api_react/frontend/public/favicon.ico +0 -0
  106. src/frontends/live_api_react/frontend/public/index.html +62 -0
  107. src/frontends/live_api_react/frontend/public/robots.txt +3 -0
  108. src/frontends/live_api_react/frontend/src/App.scss +189 -0
  109. src/frontends/live_api_react/frontend/src/App.test.tsx +25 -0
  110. src/frontends/live_api_react/frontend/src/App.tsx +205 -0
  111. src/frontends/live_api_react/frontend/src/components/audio-pulse/AudioPulse.tsx +64 -0
  112. src/frontends/live_api_react/frontend/src/components/audio-pulse/audio-pulse.scss +68 -0
  113. src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +217 -0
  114. src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +201 -0
  115. src/frontends/live_api_react/frontend/src/components/logger/Logger.tsx +241 -0
  116. src/frontends/live_api_react/frontend/src/components/logger/logger.scss +133 -0
  117. src/frontends/live_api_react/frontend/src/components/logger/mock-logs.ts +151 -0
  118. src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +161 -0
  119. src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +285 -0
  120. src/frontends/live_api_react/frontend/src/contexts/LiveAPIContext.tsx +48 -0
  121. src/frontends/live_api_react/frontend/src/hooks/use-live-api.ts +115 -0
  122. src/frontends/live_api_react/frontend/src/hooks/use-media-stream-mux.ts +23 -0
  123. src/frontends/live_api_react/frontend/src/hooks/use-screen-capture.ts +72 -0
  124. src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts +69 -0
  125. src/frontends/live_api_react/frontend/src/index.css +28 -0
  126. src/frontends/live_api_react/frontend/src/index.tsx +35 -0
  127. src/frontends/live_api_react/frontend/src/multimodal-live-types.ts +242 -0
  128. src/frontends/live_api_react/frontend/src/react-app-env.d.ts +17 -0
  129. src/frontends/live_api_react/frontend/src/reportWebVitals.ts +31 -0
  130. src/frontends/live_api_react/frontend/src/setupTests.ts +21 -0
  131. src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts +111 -0
  132. src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts +270 -0
  133. src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts +43 -0
  134. src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +329 -0
  135. src/frontends/live_api_react/frontend/src/utils/store-logger.ts +64 -0
  136. src/frontends/live_api_react/frontend/src/utils/utils.ts +86 -0
  137. src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts +73 -0
  138. src/frontends/live_api_react/frontend/src/utils/worklets/vol-meter.ts +65 -0
  139. src/frontends/live_api_react/frontend/tsconfig.json +25 -0
  140. src/frontends/streamlit/frontend/side_bar.py +213 -0
  141. src/frontends/streamlit/frontend/streamlit_app.py +263 -0
  142. src/frontends/streamlit/frontend/style/app_markdown.py +37 -0
  143. src/frontends/streamlit/frontend/utils/chat_utils.py +67 -0
  144. src/frontends/streamlit/frontend/utils/local_chat_history.py +125 -0
  145. src/frontends/streamlit/frontend/utils/message_editing.py +59 -0
  146. src/frontends/streamlit/frontend/utils/multimodal_utils.py +217 -0
  147. src/frontends/streamlit/frontend/utils/stream_handler.py +282 -0
  148. src/frontends/streamlit/frontend/utils/title_summary.py +77 -0
  149. src/resources/containers/data_processing/Dockerfile +25 -0
  150. src/resources/locks/uv-agentic_rag_vertexai_search-agent_engine.lock +4684 -0
  151. src/resources/locks/uv-agentic_rag_vertexai_search-cloud_run.lock +5799 -0
  152. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +5509 -0
  153. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +6688 -0
  154. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +4595 -0
  155. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +5710 -0
  156. src/resources/locks/uv-multimodal_live_api-cloud_run.lock +5665 -0
  157. src/resources/setup_cicd/cicd_variables.tf +36 -0
  158. src/resources/setup_cicd/github.tf +85 -0
  159. src/resources/setup_cicd/providers.tf +39 -0
  160. src/utils/generate_locks.py +135 -0
  161. src/utils/lock_utils.py +82 -0
  162. 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 .