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
@@ -11,26 +11,29 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- {% if cookiecutter.agent_name == "live_api" %}
14
+ {% if cookiecutter.agent_name == "adk_live" %}
15
15
  import asyncio
16
16
  import json
17
17
  import logging
18
18
  from collections.abc import Callable
19
19
  from pathlib import Path
20
- from typing import Any, Literal
21
20
 
22
21
  import backoff
23
22
  from fastapi import FastAPI, HTTPException, WebSocket
24
23
  from fastapi.middleware.cors import CORSMiddleware
25
24
  from fastapi.responses import FileResponse
26
25
  from fastapi.staticfiles import StaticFiles
26
+ from google.adk.agents.live_request_queue import LiveRequest, LiveRequestQueue
27
+ from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
28
+ from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
29
+ from google.adk.runners import Runner
30
+ from google.adk.sessions.in_memory_session_service import InMemorySessionService
27
31
  from google.cloud import logging as google_cloud_logging
28
- from google.genai import types
29
- from google.genai.types import LiveServerToolCall
30
- from pydantic import BaseModel
32
+ from vertexai.agent_engines import _utils
31
33
  from websockets.exceptions import ConnectionClosedError
32
34
 
33
- from .agent import MODEL_ID, genai_client, live_connect_config, tool_functions
35
+ from .agent import root_agent
36
+ from .utils.typing import Feedback
34
37
 
35
38
  app = FastAPI()
36
39
  app.add_middleware(
@@ -56,117 +59,160 @@ logger = logging_client.logger(__name__)
56
59
  logging.basicConfig(level=logging.INFO)
57
60
 
58
61
 
59
- class GeminiSession:
60
- """Manages bidirectional communication between a client and the Gemini model."""
62
+ # Initialize ADK services
63
+ session_service = InMemorySessionService()
64
+ artifact_service = InMemoryArtifactService()
65
+ memory_service = InMemoryMemoryService()
61
66
 
62
- def __init__(
63
- self, session: Any, websocket: WebSocket, tool_functions: dict[str, Callable]
64
- ) -> None:
65
- """Initialize the Gemini session.
67
+ # Initialize ADK runner
68
+ runner = Runner(
69
+ agent=root_agent,
70
+ session_service=session_service,
71
+ artifact_service=artifact_service,
72
+ memory_service=memory_service,
73
+ app_name="live-app",
74
+ )
75
+
76
+
77
+ class AgentSession:
78
+ """Manages bidirectional communication between a client and the agent."""
79
+
80
+ def __init__(self, websocket: WebSocket) -> None:
81
+ """Initialize the agent session.
66
82
 
67
83
  Args:
68
- session: The Gemini session
69
84
  websocket: The client websocket connection
70
- user_id: Unique identifier for this client
71
- tool_functions: Dictionary of available tool functions
72
85
  """
73
- self.session = session
74
86
  self.websocket = websocket
75
- self.run_id = "n/a"
76
- self.user_id = "n/a"
77
- self.tool_functions = tool_functions
78
- self._tool_tasks: list[asyncio.Task] = []
87
+ self.input_queue: asyncio.Queue[dict] = asyncio.Queue()
88
+ self.user_id: str | None = None
89
+ self.session_id: str | None = None
79
90
 
80
91
  async def receive_from_client(self) -> None:
81
- """Listen for and process messages from the client.
82
-
83
- Continuously receives messages and forwards audio data to Gemini.
84
- Handles connection errors gracefully.
85
- """
92
+ """Listen for messages from the client and put them in the queue."""
86
93
  while True:
87
94
  try:
88
- data = await self.websocket.receive_json()
89
-
90
- if isinstance(data, dict) and (
91
- "realtimeInput" in data or "clientContent" in data
92
- ):
93
- await self.session._ws.send(json.dumps(data))
94
- elif "setup" in data:
95
- self.run_id = data["setup"]["run_id"]
96
- self.user_id = data["setup"]["user_id"]
97
- logger.log_struct(
98
- {**data["setup"], "type": "setup"}, severity="INFO"
99
- )
95
+ message = await self.websocket.receive()
96
+
97
+ if "text" in message:
98
+ data = json.loads(message["text"])
99
+
100
+ if isinstance(data, dict):
101
+ # Skip setup messages - they're for backend logging only
102
+ if "setup" in data:
103
+ logger.log_struct(
104
+ {**data["setup"], "type": "setup"}, severity="INFO"
105
+ )
106
+ logging.info(
107
+ "Received setup message (not forwarding to agent)"
108
+ )
109
+ continue
110
+
111
+ # Forward message to agent engine
112
+ await self.input_queue.put(data)
113
+ else:
114
+ logging.warning(
115
+ f"Received unexpected JSON structure from client: {data}"
116
+ )
117
+
118
+ elif "bytes" in message:
119
+ # Handle binary data
120
+ await self.input_queue.put({"binary_data": message["bytes"]})
121
+
100
122
  else:
101
- logging.warning(f"Received unexpected input from client: {data}")
123
+ logging.warning(
124
+ f"Received unexpected message type from client: {message}"
125
+ )
126
+
102
127
  except ConnectionClosedError as e:
103
- logging.warning(f"Client {self.user_id} closed connection: {e}")
128
+ logging.warning(f"Client closed connection: {e}")
129
+ break
130
+ except json.JSONDecodeError as e:
131
+ logging.error(f"Error parsing JSON from client: {e}")
104
132
  break
105
133
  except Exception as e:
106
- logging.error(f"Error receiving from client {self.user_id}: {e!s}")
134
+ logging.error(f"Error receiving from client: {e!s}")
107
135
  break
108
136
 
109
- def _get_func(self, action_label: str | None) -> Callable | None:
110
- """Get the tool function for a given action label."""
111
- if action_label is None or action_label == "":
112
- return None
113
- return self.tool_functions.get(action_label)
114
-
115
- async def _handle_tool_call(
116
- self, session: Any, tool_call: LiveServerToolCall
117
- ) -> None:
118
- """Process tool calls from Gemini and send back responses."""
119
- if tool_call.function_calls is None:
120
- logging.debug("No function calls in tool_call")
121
- return
122
-
123
- for fc in tool_call.function_calls:
124
- logging.debug(f"Calling tool function: {fc.name} with args: {fc.args}")
125
- func = self._get_func(fc.name)
126
- if func is None:
127
- logging.error(f"Function {fc.name} not found")
128
- continue
129
- args = fc.args if fc.args is not None else {}
130
-
131
- # Handle both async and sync functions appropriately
132
- if asyncio.iscoroutinefunction(func):
133
- # Function is already async
134
- response = await func(**args)
135
- else:
136
- # Run sync function in a thread pool to avoid blocking
137
- response = await asyncio.to_thread(func, **args)
138
-
139
- tool_response = types.LiveClientToolResponse(
140
- function_responses=[
141
- types.FunctionResponse(name=fc.name, id=fc.id, response=response)
142
- ]
143
- )
144
- logging.debug(f"Tool response: {tool_response}")
145
- await session.send(input=tool_response)
146
-
147
- async def receive_from_gemini(self) -> None:
148
- """Listen for and process messages from Gemini without blocking."""
149
- while result := await self.session._ws.recv(decode=False):
150
- await self.websocket.send_bytes(result)
151
- raw_message = json.loads(result)
152
- if "toolCall" in raw_message:
153
- message = types.LiveServerMessage.model_validate(raw_message)
154
- tool_call = LiveServerToolCall.model_validate(message.tool_call)
155
- # Create a separate task to handle the tool call without blocking
156
- task = asyncio.create_task(
157
- self._handle_tool_call(self.session, tool_call)
137
+ async def run_agent(self) -> None:
138
+ """Run the agent with the input queue using bidi_stream_query protocol."""
139
+ try:
140
+ # Send setupComplete immediately
141
+ setup_complete_response: dict = {"setupComplete": {}}
142
+ await self.websocket.send_json(setup_complete_response)
143
+
144
+ # Wait for first request with user_id
145
+ first_request = await self.input_queue.get()
146
+ self.user_id = first_request.get("user_id")
147
+ if not self.user_id:
148
+ raise ValueError("The first request must have a user_id.")
149
+
150
+ self.session_id = first_request.get("session_id")
151
+ first_live_request = first_request.get("live_request")
152
+
153
+ # Create session if needed
154
+ if not self.session_id:
155
+ session = await session_service.create_session(
156
+ app_name="live-app",
157
+ user_id=self.user_id,
158
158
  )
159
- self._tool_tasks.append(task)
159
+ self.session_id = session.id
160
+
161
+ # Create LiveRequestQueue
162
+ live_request_queue = LiveRequestQueue()
163
+
164
+ # Add first live request if present
165
+ if first_live_request and isinstance(first_live_request, dict):
166
+ live_request_queue.send(LiveRequest.model_validate(first_live_request))
167
+
168
+ # Forward requests from input_queue to live_request_queue
169
+ async def _forward_requests() -> None:
170
+ while True:
171
+ request = await self.input_queue.get()
172
+ live_request = LiveRequest.model_validate(request)
173
+ live_request_queue.send(live_request)
174
+
175
+ # Forward events from agent to websocket
176
+ async def _forward_events() -> None:
177
+ events_async = runner.run_live(
178
+ user_id=self.user_id,
179
+ session_id=self.session_id,
180
+ live_request_queue=live_request_queue,
181
+ )
182
+ async for event in events_async:
183
+ event_dict = _utils.dump_event_for_json(event)
184
+ await self.websocket.send_json(event_dict)
185
+
186
+ # Check for error responses
187
+ if isinstance(event_dict, dict) and "error" in event_dict:
188
+ logging.error(f"Agent error: {event_dict['error']}")
189
+ break
190
+
191
+ # Run both tasks
192
+ requests_task = asyncio.create_task(_forward_requests())
193
+
194
+ try:
195
+ await _forward_events()
196
+ finally:
197
+ requests_task.cancel()
198
+ try:
199
+ await requests_task
200
+ except asyncio.CancelledError:
201
+ pass
202
+
203
+ except Exception as e:
204
+ logging.error(f"Error in agent: {e}")
205
+ await self.websocket.send_json({"error": str(e)})
160
206
 
161
207
 
162
208
  def get_connect_and_run_callable(websocket: WebSocket) -> Callable:
163
- """Create a callable that handles Gemini connection with retry logic.
209
+ """Create a callable that handles agent connection with retry logic.
164
210
 
165
211
  Args:
166
212
  websocket: The client websocket connection
167
213
 
168
214
  Returns:
169
- Callable: An async function that establishes and manages the Gemini connection
215
+ Callable: An async function that establishes and manages the agent connection
170
216
  """
171
217
 
172
218
  async def on_backoff(details: backoff._typing.Details) -> None:
@@ -180,18 +226,14 @@ def get_connect_and_run_callable(websocket: WebSocket) -> Callable:
180
226
  backoff.expo, ConnectionClosedError, max_tries=10, on_backoff=on_backoff
181
227
  )
182
228
  async def connect_and_run() -> None:
183
- async with genai_client.aio.live.connect(
184
- model=MODEL_ID, config=live_connect_config
185
- ) as session:
186
- await websocket.send_json({"status": "Backend is ready for conversation"})
187
- gemini_session = GeminiSession(
188
- session=session, websocket=websocket, tool_functions=tool_functions
189
- )
190
- logging.info("Starting bidirectional communication")
191
- await asyncio.gather(
192
- gemini_session.receive_from_client(),
193
- gemini_session.receive_from_gemini(),
194
- )
229
+ logging.info("Starting ADK agent")
230
+ session = AgentSession(websocket)
231
+
232
+ logging.info("Starting bidirectional communication with agent")
233
+ await asyncio.gather(
234
+ session.receive_from_client(),
235
+ session.run_agent(),
236
+ )
195
237
 
196
238
  return connect_and_run
197
239
 
@@ -204,15 +246,38 @@ async def websocket_endpoint(websocket: WebSocket) -> None:
204
246
  await connect_and_run()
205
247
 
206
248
 
207
- class Feedback(BaseModel):
208
- """Represents feedback for a conversation."""
249
+ @app.get("/")
250
+ async def serve_frontend_root() -> FileResponse:
251
+ """Serve the frontend index.html at the root path."""
252
+ index_file = frontend_build_dir / "index.html"
253
+ if index_file.exists():
254
+ return FileResponse(str(index_file))
255
+ raise HTTPException(
256
+ status_code=404,
257
+ detail="Frontend not built. Run 'npm run build' in the frontend directory.",
258
+ )
259
+
260
+
261
+ @app.get("/{full_path:path}")
262
+ async def serve_frontend_spa(full_path: str) -> FileResponse:
263
+ """Catch-all route to serve the frontend for SPA routing.
264
+
265
+ This ensures that client-side routes are handled by the React app.
266
+ Excludes API routes (ws, feedback) and static assets.
267
+ """
268
+ # Don't intercept API routes
269
+ if full_path.startswith(("ws", "feedback", "static", "api")):
270
+ raise HTTPException(status_code=404, detail="Not found")
209
271
 
210
- score: int | float
211
- text: str | None = ""
212
- run_id: str
213
- user_id: str | None
214
- log_type: Literal["feedback"] = "feedback"
215
- {% elif "adk" in cookiecutter.tags %}
272
+ # Serve index.html for all other routes (SPA routing)
273
+ index_file = frontend_build_dir / "index.html"
274
+ if index_file.exists():
275
+ return FileResponse(str(index_file))
276
+ raise HTTPException(
277
+ status_code=404,
278
+ detail="Frontend not built. Run 'npm run build' in the frontend directory.",
279
+ )
280
+ {% elif cookiecutter.is_adk %}
216
281
  import os
217
282
 
218
283
  import google.auth
@@ -358,7 +423,7 @@ def stream_messages(
358
423
  set_tracing_properties(config)
359
424
  input_dict = input.model_dump()
360
425
 
361
- for data in agent.stream(input_dict, config=config, stream_mode="messages"): # type: ignore[arg-type]
426
+ for data in agent.stream(input_dict, config=config, stream_mode="messages"):
362
427
  yield dumps(data) + "\n"
363
428
 
364
429
 
@@ -397,40 +462,7 @@ def collect_feedback(feedback: Feedback) -> dict[str, str]:
397
462
  """
398
463
  logger.log_struct(feedback.model_dump(), severity="INFO")
399
464
  return {"status": "success"}
400
- {% if cookiecutter.agent_name == "live_api" %}
401
-
402
- @app.get("/")
403
- async def serve_frontend_root() -> FileResponse:
404
- """Serve the frontend index.html at the root path."""
405
- index_file = frontend_build_dir / "index.html"
406
- if index_file.exists():
407
- return FileResponse(str(index_file))
408
- raise HTTPException(
409
- status_code=404,
410
- detail="Frontend not built. Run 'npm run build' in the frontend directory.",
411
- )
412
-
413
465
 
414
- @app.get("/{full_path:path}")
415
- async def serve_frontend_spa(full_path: str) -> FileResponse:
416
- """Catch-all route to serve the frontend for SPA routing.
417
-
418
- This ensures that client-side routes are handled by the React app.
419
- Excludes API routes (ws, feedback) and static assets.
420
- """
421
- # Don't intercept API routes
422
- if full_path.startswith(("ws", "feedback", "static", "api")):
423
- raise HTTPException(status_code=404, detail="Not found")
424
-
425
- # Serve index.html for all other routes (SPA routing)
426
- index_file = frontend_build_dir / "index.html"
427
- if index_file.exists():
428
- return FileResponse(str(index_file))
429
- raise HTTPException(
430
- status_code=404,
431
- detail="Frontend not built. Run 'npm run build' in the frontend directory.",
432
- )
433
- {% endif %}
434
466
 
435
467
  # Main execution
436
468
  if __name__ == "__main__":