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.

Files changed (101) hide show
  1. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/METADATA +2 -2
  2. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/RECORD +96 -94
  3. agents/adk_base/.template/templateconfig.yaml +1 -1
  4. agents/{live_api → adk_live}/.template/templateconfig.yaml +5 -7
  5. agents/adk_live/README.md +31 -0
  6. agents/adk_live/app/agent.py +48 -0
  7. agents/adk_live/tests/unit/test_dummy.py +38 -0
  8. agents/agentic_rag/.template/templateconfig.yaml +1 -1
  9. agents/crewai_coding_crew/app/agent.py +18 -57
  10. agents/langgraph_base_react/app/agent.py +7 -46
  11. llm.txt +1 -1
  12. src/base_template/GEMINI.md +1 -1
  13. src/base_template/Makefile +130 -61
  14. src/base_template/README.md +6 -6
  15. src/base_template/deployment/terraform/dev/apis.tf +1 -1
  16. src/base_template/deployment/terraform/dev/variables.tf +1 -1
  17. src/base_template/deployment/terraform/locals.tf +1 -1
  18. src/base_template/deployment/terraform/variables.tf +1 -1
  19. src/base_template/pyproject.toml +22 -21
  20. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +2 -2
  21. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/pr_checks.yaml +1 -1
  22. src/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +71 -8
  23. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +2 -2
  24. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/pr_checks.yaml +1 -1
  25. src/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +90 -8
  26. src/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py +1 -1
  27. src/base_template/{{cookiecutter.agent_directory}}/utils/typing.py +4 -4
  28. src/cli/utils/template.py +12 -5
  29. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +205 -4
  30. src/deployment_targets/agent_engine/tests/load_test/README.md +47 -0
  31. src/deployment_targets/agent_engine/tests/load_test/load_test.py +132 -3
  32. src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +11 -3
  33. src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +5 -1
  34. 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
  35. src/deployment_targets/cloud_run/Dockerfile +3 -3
  36. src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +4 -4
  37. src/deployment_targets/cloud_run/deployment/terraform/service.tf +7 -7
  38. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +207 -5
  39. src/deployment_targets/cloud_run/tests/load_test/README.md +82 -0
  40. src/deployment_targets/cloud_run/tests/load_test/load_test.py +130 -3
  41. src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +178 -146
  42. src/frontends/{live_api_react → adk_live_react}/frontend/package-lock.json +39 -1007
  43. src/frontends/{live_api_react → adk_live_react}/frontend/package.json +1 -9
  44. src/frontends/{live_api_react → adk_live_react}/frontend/src/App.tsx +1 -1
  45. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/Logger.tsx +8 -3
  46. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/logger.scss +26 -0
  47. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/side-panel/SidePanel.tsx +11 -5
  48. src/frontends/{live_api_react → adk_live_react}/frontend/src/components/side-panel/side-panel.scss +146 -115
  49. src/frontends/adk_live_react/frontend/src/components/transcription-preview/TranscriptionPreview.tsx +106 -0
  50. src/frontends/adk_live_react/frontend/src/components/transcription-preview/transcription-preview.scss +150 -0
  51. src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-live-api.ts +8 -2
  52. src/frontends/{live_api_react → adk_live_react}/frontend/src/multimodal-live-types.ts +38 -2
  53. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audio-recorder.ts +1 -1
  54. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audio-streamer.ts +1 -1
  55. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/multimodal-live-client.ts +204 -23
  56. src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/utils.ts +27 -5
  57. src/frontends/streamlit/frontend/utils/local_chat_history.py +2 -0
  58. src/resources/idx/.idx/dev.nix +25 -11
  59. src/resources/idx/idx-template.json +1 -16
  60. src/resources/idx/idx-template.nix +2 -3
  61. src/resources/locks/uv-adk_base-agent_engine.lock +434 -349
  62. src/resources/locks/uv-adk_base-cloud_run.lock +502 -409
  63. src/resources/locks/uv-adk_live-agent_engine.lock +4189 -0
  64. src/resources/locks/{uv-live_api-cloud_run.lock → uv-adk_live-cloud_run.lock} +884 -2219
  65. src/resources/locks/uv-agentic_rag-agent_engine.lock +473 -388
  66. src/resources/locks/uv-agentic_rag-cloud_run.lock +557 -464
  67. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +498 -515
  68. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +898 -687
  69. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +455 -483
  70. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +910 -645
  71. src/utils/generate_locks.py +8 -4
  72. agents/live_api/README.md +0 -37
  73. agents/live_api/app/agent.py +0 -72
  74. agents/live_api/tests/integration/test_server_e2e.py +0 -260
  75. agents/live_api/tests/load_test/load_test.py +0 -40
  76. agents/live_api/tests/unit/test_server.py +0 -144
  77. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/WHEEL +0 -0
  78. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/entry_points.txt +0 -0
  79. {agent_starter_pack-0.15.7.dist-info → agent_starter_pack-0.16.0.dist-info}/licenses/LICENSE +0 -0
  80. /src/frontends/{live_api_react → adk_live_react}/frontend/public/favicon.ico +0 -0
  81. /src/frontends/{live_api_react → adk_live_react}/frontend/public/index.html +0 -0
  82. /src/frontends/{live_api_react → adk_live_react}/frontend/public/robots.txt +0 -0
  83. /src/frontends/{live_api_react → adk_live_react}/frontend/src/App.scss +0 -0
  84. /src/frontends/{live_api_react → adk_live_react}/frontend/src/App.test.tsx +0 -0
  85. /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/audio-pulse/AudioPulse.tsx +0 -0
  86. /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/audio-pulse/audio-pulse.scss +0 -0
  87. /src/frontends/{live_api_react → adk_live_react}/frontend/src/components/logger/mock-logs.ts +0 -0
  88. /src/frontends/{live_api_react → adk_live_react}/frontend/src/contexts/LiveAPIContext.tsx +0 -0
  89. /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-media-stream-mux.ts +0 -0
  90. /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-screen-capture.ts +0 -0
  91. /src/frontends/{live_api_react → adk_live_react}/frontend/src/hooks/use-webcam.ts +0 -0
  92. /src/frontends/{live_api_react → adk_live_react}/frontend/src/index.css +0 -0
  93. /src/frontends/{live_api_react → adk_live_react}/frontend/src/index.tsx +0 -0
  94. /src/frontends/{live_api_react → adk_live_react}/frontend/src/react-app-env.d.ts +0 -0
  95. /src/frontends/{live_api_react → adk_live_react}/frontend/src/reportWebVitals.ts +0 -0
  96. /src/frontends/{live_api_react → adk_live_react}/frontend/src/setupTests.ts +0 -0
  97. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/audioworklet-registry.ts +0 -0
  98. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/store-logger.ts +0 -0
  99. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/worklets/audio-processing.ts +0 -0
  100. /src/frontends/{live_api_react → adk_live_react}/frontend/src/utils/worklets/vol-meter.ts +0 -0
  101. /src/frontends/{live_api_react → adk_live_react}/frontend/tsconfig.json +0 -0
@@ -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": list(config.get("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
- ![live_api_diagram](https://storage.googleapis.com/github-repo/generative-ai/sample-apps/e2e-gen-ai-app-starter-pack/live_api_diagram.png)
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
- ![live api demo](https://storage.googleapis.com/github-repo/generative-ai/sample-apps/e2e-gen-ai-app-starter-pack/live_api_pattern_demo.gif)
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.
@@ -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"