agent-starter-pack 0.12.1__py3-none-any.whl → 0.13.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-starter-pack
3
- Version: 0.12.1
3
+ Version: 0.13.0
4
4
  Summary: CLI to bootstrap production-ready Google Cloud GenAI agent projects from templates.
5
5
  Author-email: Google LLC <agent-starter-pack@google.com>
6
6
  License: Apache-2.0
@@ -38,8 +38,8 @@ agents/live_api/tests/unit/test_server.py,sha256=_tf9nHkb7aOe3lZRlyp7cVklOTOYTuX
38
38
  llm.txt,sha256=onvCAhaPcye3YWfCkQeX4Y-LWF1TsAsZ09cO_iO-i3Y,14607
39
39
  src/base_template/.gitignore,sha256=Fq0w34x4sfbwP4RqDqh6hdHNYRUEsFNI-9XONzLWBIs,2580
40
40
  src/base_template/GEMINI.md,sha256=WzscHWlQeYkKORZ-453P8KM9IHuj1kAxW-69c7Ytuwk,133
41
- src/base_template/Makefile,sha256=tUPxvZI-XyAs2cXa3L3aTh3DFkwUBYkF5BOGs0DtvIk,6267
42
- src/base_template/README.md,sha256=KkHKYpnkU7gzfvzyULlTyt7gUxkf1IcK7Y4ydceLVzc,11458
41
+ src/base_template/Makefile,sha256=8LRvbHvuj92oFr9dB3E92O39WqMKxM_kPO5t2lewOC4,6818
42
+ src/base_template/README.md,sha256=ywTmrlWmivgHAtzXQCZqNu_Hfxp51QTnX4USNgGk6kU,10040
43
43
  src/base_template/pyproject.toml,sha256=dTyxaWetB-XCKQEDV3lXxAUxtBEpustNdlAEcT8ZZpo,2932
44
44
  src/base_template/deployment/README.md,sha256=gZJvSWdQh_Mi-bZ3dmuPv7fMezIw04fgN5tq7KgglPw,692
45
45
  src/base_template/deployment/terraform/apis.tf,sha256=KX8Oe2gjT-gh9Bkntz9yIAyRjPc1gZvwOhroJ6NZVp4,1513
@@ -72,17 +72,17 @@ src/base_template/{{cookiecutter.agent_directory}}/utils/gcs.py,sha256=jKblaWOGQ
72
72
  src/base_template/{{cookiecutter.agent_directory}}/utils/tracing.py,sha256=2rv1Ukh2jTBENDwoghCItJ28l-Sjz9gMlzdojlVgJa4,6052
73
73
  src/base_template/{{cookiecutter.agent_directory}}/utils/typing.py,sha256=DP5OZC3IGvqA1XbvWt8kI3gyAK3ZjzUSL5Ca17wNeLI,4249
74
74
  src/cli/main.py,sha256=Dya7Sw3ozMTaGDcwMh_-W7udkGZHGzgAj8aBdSZaZxI,1832
75
- src/cli/commands/create.py,sha256=wo72wjnHOaC5IVrDKtXzWa0YlHsXGCAyTpGlq-DUVq8,44892
75
+ src/cli/commands/create.py,sha256=qVcK3E5DzQ66QACrwDA2q-6LvCApipIcht9HCi-LT28,45331
76
76
  src/cli/commands/enhance.py,sha256=4QMfU2hGHw1tZLrTYtBvpc3n6__mdSSekKMqy4OQAyE,15137
77
- src/cli/commands/list.py,sha256=Z2QCUMTG8Bwi5suSOkX4aJO6EReTaghiYMJtjj6PtNA,5584
77
+ src/cli/commands/list.py,sha256=ZGol9eYB9Yon7JysMUCtpEOwdXzrApdTHzErx6KvT04,6856
78
78
  src/cli/commands/setup_cicd.py,sha256=nSgMUD4_ncYGwLWU1Fl7Ypw3GTJc8qVfjPxwZMLn4xo,32113
79
79
  src/cli/utils/__init__.py,sha256=_cTmsXGPqOtK0q8UW5164QTltbJRJFR_Efxq_BRL1-o,1311
80
80
  src/cli/utils/cicd.py,sha256=9s_OcusQznT_pSjFP60BfDBoZ5V6bwPE0QWbWdMaVlY,26515
81
81
  src/cli/utils/datastores.py,sha256=gv1V6eDcOEKx4MRNG5C3Y-VfixYq1AzQuaYMLp8QRNo,1058
82
- src/cli/utils/gcp.py,sha256=cnuCyN144eiyYc9aJNEK9JnyWN66rdevugoMdDYC1UU,4032
82
+ src/cli/utils/gcp.py,sha256=XDtQVsqnYiZl4OGhrDjRwn5vq4GpFGu9v1CRwdu39Rs,8245
83
83
  src/cli/utils/logging.py,sha256=MYPxQYXXaKCqBVOfrhEMe8lFKzpzZWjOVP2Km81X5Mk,3007
84
- src/cli/utils/remote_template.py,sha256=d_kRMWhZkuKGCiQPWuMQDgRsoHTZXgep-dAhxfbV9Is,12246
85
- src/cli/utils/template.py,sha256=2CnxXTvQVM_ZnY8NlBNXuyhmIsflDe1eTUrHPFR7Qqk,46175
84
+ src/cli/utils/remote_template.py,sha256=BYdV9Bgj54UUgOTnxlkOr8xA9ZgqKSEE-lWnmpJ05UY,18529
85
+ src/cli/utils/template.py,sha256=j-f1FyvG9BuE7KTZglQ9BCYiPMbvW5z2xnr5JQLOhkg,50034
86
86
  src/cli/utils/version.py,sha256=F4udQmzniPStqWZFIgnv3Qg3l9non4mfy2An-Oveqmc,2916
87
87
  src/data_ingestion/README.md,sha256=LNxSQoJW9JozK-TbyGQLj5L_MGWNwrfLk6V6RmQ2oBQ,4032
88
88
  src/data_ingestion/pyproject.toml,sha256=-1Mf2QB8K70ICQV5UPZDpf-fN3UwEQLVzQyxfakCSTY,445
@@ -98,14 +98,14 @@ src/deployment_targets/agent_engine/tests/load_test/README.md,sha256=aQP7nDAqd2j
98
98
  src/deployment_targets/agent_engine/tests/load_test/load_test.py,sha256=USzS89bQ4qHVoQynDRSRShKzeXf1MJ0MBV4FpV40vrI,4249
99
99
  src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
100
  src/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py,sha256=91idmH-p7B7Gk8iMZoUeutndUqBhXt3apxUrTE64VlM,12797
101
- src/deployment_targets/cloud_run/Dockerfile,sha256=LdVMFTcYtO7b0L3jBXbVMUlHknL41ELp3RmzIGmxOH8,978
101
+ src/deployment_targets/cloud_run/Dockerfile,sha256=rSpmFK7uJhZYwYMtxH8W7mywcCPvaoFn7gL_mXRYuF8,1449
102
102
  src/deployment_targets/cloud_run/deployment/terraform/service.tf,sha256=w3iEnyScbO0sjLPQEYfs3GmPpHTUMGeG1ghi09wSY24,10322
103
103
  src/deployment_targets/cloud_run/deployment/terraform/dev/service.tf,sha256=VegAzMcl2ohxMvtq6bcSXfCHYzQW68usyA839oULpgk,6581
104
104
  src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py,sha256=85tA1gtbu_Z2CJqzOHgvDS-lvQFbYUMW8_mfqEc1_8I,9325
105
105
  src/deployment_targets/cloud_run/tests/load_test/README.md,sha256=clrCwgwUDr_AsivG0oLeNImTK4uke9EAWkrpS2nhIX0,2769
106
106
  src/deployment_targets/cloud_run/tests/load_test/load_test.py,sha256=kK3KaCdBFHKlni3ZVY2u4h_ugNla4o6pbhGRZlQ-haI,4270
107
107
  src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder,sha256=ZbWpSgDo8bT33PstD_73aQSeN_0oo0F104NMUGuZ8lE,14903
108
- src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py,sha256=kKmjr8opUz2Z1nFw9-yKCu8T0tCBZQgWm3R9ab_l1fM,13752
108
+ src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py,sha256=TKUFGNVxBlWh61owDIemiE734ENWKtWHXSAZ5Oix2t0,15463
109
109
  src/frontends/live_api_react/frontend/package-lock.json,sha256=h6yK6e_GR6CeZn_C9gGJoMpLjuBExjiDFIEaC28b6lU,734344
110
110
  src/frontends/live_api_react/frontend/package.json,sha256=OBOzzDiDiESPzmbLDlZ6KM1Trit71vXdP692dI-g9Uo,1381
111
111
  src/frontends/live_api_react/frontend/tsconfig.json,sha256=cyqEhf7-Yydz-PX8_cuF8JpsyC363NDTNkrmCk0sKAo,595
@@ -114,7 +114,7 @@ src/frontends/live_api_react/frontend/public/index.html,sha256=tOhOQOx5kMi9WhxT-
114
114
  src/frontends/live_api_react/frontend/public/robots.txt,sha256=kNJLw79pisHhc3OVAimMzKcq3x9WT6sF9IS4xI0crdI,67
115
115
  src/frontends/live_api_react/frontend/src/App.scss,sha256=477MjERLsjhhzSS5WvROHfgdLi41F0PeW7zFTIROlH4,3472
116
116
  src/frontends/live_api_react/frontend/src/App.test.tsx,sha256=l4bj_dLHDggvlcxZZgbhXdR9oIpdCwrGglRWtSqNqUU,869
117
- src/frontends/live_api_react/frontend/src/App.tsx,sha256=5PhZTXo4gwWRvkF5dJTwu4YpWKs43bsLSl1IGXx1edY,7261
117
+ src/frontends/live_api_react/frontend/src/App.tsx,sha256=ZPGBW_hieBDLmGO4_d3DWPAin8SU1QNy4G-YkZhY6sw,7388
118
118
  src/frontends/live_api_react/frontend/src/index.css,sha256=THy1DxvcOTW4WypzWvYEL_SVAVzda1iPEQ0M6pvgNUo,950
119
119
  src/frontends/live_api_react/frontend/src/index.tsx,sha256=c3VCNMXzTZInNqpHaQUMPPwZsicO468Ujc5u3jLGLZ8,1150
120
120
  src/frontends/live_api_react/frontend/src/multimodal-live-types.ts,sha256=J52j9vfza9EyODprh9w29psdBSEygTnXZF8yJIsfsDs,6037
@@ -138,7 +138,7 @@ src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts,sha256=NuTM7meIwaS
138
138
  src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts,sha256=JcO69YeM7boSf1gM5VVX6cbLvGRT-dYYJjWUVBJFWbM,3699
139
139
  src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts,sha256=xgtnrm_jbmKzwUkeCQiv3C7jmU7DfdaJU2su6NLpXCk,8301
140
140
  src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts,sha256=2I1va0WszQ3-SHfSmgp5-ToOSof5H6q-rgFnlf8mGUI,1258
141
- src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts,sha256=afOZaVZdPrTFTvZj3m1I463pBqfyd9CI6wvctieoG90,9771
141
+ src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts,sha256=u9gVQCXtSlylnDEd9GsQ1CkCTc_1kLeZD1WdgzcGzqc,9946
142
142
  src/frontends/live_api_react/frontend/src/utils/store-logger.ts,sha256=YxS0TjiGntFPIrHVVU14WplpITzzxP2faTeAVOMvbQA,1761
143
143
  src/frontends/live_api_react/frontend/src/utils/utils.ts,sha256=qQIhLzfyCBLecU0ksQCKIbD3cIflb0hxt0SPZGdYFEo,2457
144
144
  src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts,sha256=ULgnXphZUfbHkRhGoPT_670WHjzaXJwWgYU0ISQRSXI,1979
@@ -170,8 +170,8 @@ src/resources/locks/uv-live_api-cloud_run.lock,sha256=AMgJt_aeoXU3LDvZv4iph9XCVs
170
170
  src/utils/generate_locks.py,sha256=6V1B8V2BEuevWnXUsxZVTrLjXwFRII8UfsIGrQqZxVs,4320
171
171
  src/utils/lock_utils.py,sha256=IFOMUWtb-ypm2Y8w8J5y2oI_-MaPuwPF_JOAAlnNudA,2275
172
172
  src/utils/watch_and_rebuild.py,sha256=vP4yIiA7E_lj5sfQdJUl8TXas6V7msDg8XWUutAC05Q,6679
173
- agent_starter_pack-0.12.1.dist-info/METADATA,sha256=srgIqHc7W8LUvdtlBVcLEg4PN03Z7KwBhW0oIYy8sY0,11155
174
- agent_starter_pack-0.12.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
175
- agent_starter_pack-0.12.1.dist-info/entry_points.txt,sha256=U7uCxR7YulIhZ0L8R8Hui0Bsy6J7oyESBeDYJYMrQjA,56
176
- agent_starter_pack-0.12.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
177
- agent_starter_pack-0.12.1.dist-info/RECORD,,
173
+ agent_starter_pack-0.13.0.dist-info/METADATA,sha256=ppWiEr8G2BZkql1PwhCJ3KAmfeI256SngwrYlpyGySg,11155
174
+ agent_starter_pack-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
175
+ agent_starter_pack-0.13.0.dist-info/entry_points.txt,sha256=U7uCxR7YulIhZ0L8R8Hui0Bsy6J7oyESBeDYJYMrQjA,56
176
+ agent_starter_pack-0.13.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
177
+ agent_starter_pack-0.13.0.dist-info/RECORD,,
@@ -4,7 +4,7 @@ install:
4
4
  {%- if cookiecutter.settings.get("commands", {}).get("override", {}).get("install") %}
5
5
  {{cookiecutter.settings.get("commands", {}).get("override", {}).get("install")}}
6
6
  {%- else %}
7
- uv sync --dev{% if cookiecutter.agent_name != 'live_api' and "adk" not in cookiecutter.tags %} --extra streamlit{%- endif %} --extra jupyter{% if cookiecutter.agent_name == 'live_api' %} && (cd frontend && npm install){%- endif %}
7
+ uv sync --dev{% if cookiecutter.agent_name != 'live_api' and "adk" not in cookiecutter.tags %} --extra streamlit{%- endif %}{% if cookiecutter.agent_name == 'live_api' %} && (cd frontend && npm install){%- endif %}
8
8
  {%- endif %}
9
9
 
10
10
  {%- if cookiecutter.settings.get("commands", {}).get("extra", {}) %}
@@ -23,14 +23,23 @@ install:
23
23
  {%- endif %}
24
24
  {%- endfor %}{%- endif %}
25
25
 
26
+ {%- if cookiecutter.agent_name == 'live_api' %}
27
+ # Build the frontend for production
28
+ build-frontend:
29
+ (cd frontend && npm run build)
30
+
31
+ {%- endif %}
26
32
  # Launch local dev playground
27
- playground:
33
+ playground:{%- if cookiecutter.agent_name == 'live_api' %} build-frontend{%- endif %}
28
34
  {%- if cookiecutter.settings.get("commands", {}).get("override", {}).get("playground") %}
29
35
  {{cookiecutter.settings.get("commands", {}).get("override", {}).get("playground")}}
30
36
  {%- else %}
31
37
  @echo "==============================================================================="
32
38
  @echo "| 🚀 Starting your agent playground... |"
33
39
  @echo "| |"
40
+ {%- if cookiecutter.agent_name == 'live_api' %}
41
+ @echo "| 🌐 Access your app at: http://localhost:8000 |"
42
+ {%- endif %}
34
43
  @echo "| 💡 Try asking: {{cookiecutter.example_question}}|"
35
44
  {%- if "adk" in cookiecutter.tags %}
36
45
  @echo "| |"
@@ -41,11 +50,13 @@ playground:
41
50
  uv run adk web . --port 8501
42
51
  {%- else %}
43
52
  {%- if cookiecutter.deployment_target == 'cloud_run' %}
44
- uv run uvicorn {{cookiecutter.agent_directory}}.server:app --host 0.0.0.0 --port 8000 --reload &
45
- {%- endif %}
46
53
  {%- if cookiecutter.agent_name == 'live_api' %}
47
- (cd frontend && PORT=8501 npm start)
54
+ uv run uvicorn {{cookiecutter.agent_directory}}.server:app --host 0.0.0.0 --port 8000 --reload
48
55
  {%- else %}
56
+ uv run uvicorn {{cookiecutter.agent_directory}}.server:app --host 0.0.0.0 --port 8000 --reload &
57
+ {%- endif %}
58
+ {%- endif %}
59
+ {%- if cookiecutter.agent_name != 'live_api' %}
49
60
  {% 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
50
61
  {%- endif %}
51
62
  {%- endif %}
@@ -84,7 +95,7 @@ local-backend:
84
95
  {%- if cookiecutter.deployment_target == 'cloud_run' %}
85
96
  {%- if cookiecutter.agent_name == 'live_api' %}
86
97
 
87
- # Start the frontend UI for development
98
+ # Start the frontend UI separately for development (requires backend running separately)
88
99
  ui:
89
100
  (cd frontend && PORT=8501 npm start)
90
101
  {%- endif %}
@@ -122,6 +133,7 @@ test:
122
133
 
123
134
  # Run code quality checks (codespell, ruff, mypy)
124
135
  lint:
136
+ uv sync --dev --extra lint
125
137
  uv run codespell
126
138
  uv run ruff check . --diff
127
139
  uv run ruff format . --check --diff
@@ -92,12 +92,12 @@ Here’s the recommended workflow for local development:
92
92
  make install
93
93
  ```
94
94
 
95
- 2. **Start the Backend Server:**
96
- Open a terminal and run:
95
+ 2. **Start the Full Stack Server:**
96
+ The FastAPI server now serves both the backend API and frontend interface:
97
97
  ```bash
98
98
  make local-backend
99
99
  ```
100
- The backend is ready when you see `INFO: Application startup complete.` Wait for this message before starting the frontend.
100
+ The server is ready when you see `INFO: Application startup complete.` The frontend will be available at `http://localhost:8000`.
101
101
 
102
102
  <details>
103
103
  <summary><b>Optional: Use AI Studio / API Key instead of Vertex AI</b></summary>
@@ -113,47 +113,23 @@ Here’s the recommended workflow for local development:
113
113
  </details>
114
114
  <br>
115
115
 
116
- 3. **Start the Frontend UI:**
117
- Open *another* terminal and run:
116
+ <details>
117
+ <summary><b>Alternative: Run Frontend Separately</b></summary>
118
+
119
+ If you prefer to run the frontend separately (useful for frontend development), you can still use:
118
120
  ```bash
119
121
  make ui
120
122
  ```
121
- This launches the Streamlit application, which connects to the backend server (by default at `http://localhost:8000`).
123
+ This launches the Streamlit application, which connects to the backend server at `http://localhost:8000`.
124
+ </details>
125
+ <br>
122
126
 
123
- 4. **Interact and Iterate:**
124
- * Open the Streamlit UI in your browser (usually `http://localhost:8501` or `http://localhost:3001`).
127
+ 3. **Interact and Iterate:**
128
+ * Open your browser and navigate to `http://localhost:8000` to access the integrated frontend.
125
129
  * Click the play button in the UI to connect to the backend.
126
130
  * Interact with the agent! Try prompts like: *"Using the tool you have, define Governance in the context MLOPs"*
127
131
  * Modify the agent logic in `{{cookiecutter.agent_directory}}/agent.py`. The backend server (FastAPI with `uvicorn --reload`) should automatically restart when you save changes. Refresh the frontend if needed to see behavioral changes.
128
132
 
129
- <details>
130
- <summary><b>Cloud Shell Usage</b></summary>
131
-
132
- To run the agent using Google Cloud Shell:
133
-
134
- 1. **Start the Frontend:**
135
- In a Cloud Shell tab, run:
136
- ```bash
137
- make ui
138
- ```
139
- Accept prompts to use a different port if 3000 is busy. Click the `localhost:PORT` link for the web preview.
140
-
141
- 2. **Start the Backend:**
142
- Open a *new* Cloud Shell tab. Set your project: `gcloud config set project [PROJECT_ID]`. Then run:
143
- ```bash
144
- make local-backend
145
- ```
146
-
147
- 3. **Configure Backend Web Preview:**
148
- Use the Cloud Shell Web Preview feature to expose port 8000. Change the default port from 8080 to 8000. See [Cloud Shell Web Preview documentation](https://cloud.google.com/shell/docs/using-web-preview#preview_the_application).
149
-
150
- 4. **Connect Frontend to Backend:**
151
- * Copy the URL generated by the backend web preview (e.g., `https://8000-cs-....cloudshell.dev/`).
152
- * Paste this URL into the "Server URL" field in the frontend UI settings (in the first tab).
153
- * Click the "Play button" to connect.
154
-
155
- * **Note:** The feedback feature in the frontend might not work reliably in Cloud Shell due to cross-origin issues between the preview URLs.
156
- </details>
157
133
 
158
134
  </details>
159
135
  {%- else %}
@@ -183,18 +159,7 @@ gcloud config set project <your-dev-project-id>
183
159
  make backend
184
160
  ```
185
161
  {% if cookiecutter.agent_name == 'live_api' %}
186
- **Accessing the Deployed Backend Locally:**
187
-
188
- To connect your local frontend (`make ui`) to the backend deployed on Cloud Run, use the `gcloud` proxy:
189
-
190
- 1. **Start the proxy:**
191
- ```bash
192
- # Replace with your actual service name, project, and region
193
- gcloud run services proxy gemini-agent-service --port 8000 --project $PROJECT_ID --region $REGION
194
- ```
195
- Keep this terminal running.
196
-
197
- 2. **Connect Frontend:** Your deployed backend is now accessible locally at `http://localhost:8000`. Point your Streamlit UI to this address.
162
+ **Note:** For secure access to your deployed backend, consider using Identity-Aware Proxy (IAP) by running `make backend IAP=true`.
198
163
  {%- endif %}
199
164
 
200
165
  The repository includes a Terraform configuration for the setup of the Dev Google Cloud project.
@@ -18,17 +18,11 @@ import os
18
18
  import pathlib
19
19
  import shutil
20
20
  import subprocess
21
- import sys
22
21
  import tempfile
23
22
  from collections.abc import Callable
24
23
 
25
24
  import click
26
25
  from click.core import ParameterSource
27
-
28
- if sys.version_info >= (3, 11):
29
- import tomllib
30
- else:
31
- import tomli as tomllib
32
26
  from rich.console import Console
33
27
  from rich.prompt import IntPrompt, Prompt
34
28
 
@@ -249,6 +243,8 @@ def create(
249
243
  ) -> None:
250
244
  """Create GCP-based AI agent projects from templates."""
251
245
  try:
246
+ console = Console()
247
+
252
248
  # Display welcome banner (unless skipped)
253
249
  if not skip_welcome:
254
250
  display_welcome_banner(agent)
@@ -312,6 +308,7 @@ def create(
312
308
  selected_agent = None
313
309
  template_source_path = None
314
310
  temp_dir_to_clean = None
311
+ remote_spec = None
315
312
 
316
313
  if agent:
317
314
  if agent.startswith("local@"):
@@ -353,6 +350,20 @@ def create(
353
350
  )
354
351
  temp_dir_to_clean = str(temp_dir_path)
355
352
  selected_agent = f"remote_{hash(agent)}" # Generate unique name for remote template
353
+
354
+ # Show informational message for ADK samples with smart defaults
355
+ if remote_spec.is_adk_samples:
356
+ config = load_remote_template_config(
357
+ template_source_path, is_adk_sample=True
358
+ )
359
+ if not config.get("has_explicit_config", True):
360
+ console = Console()
361
+ console.print(
362
+ "\n[blue]ℹ️ Note: The starter pack uses heuristics to template this ADK sample agent.[/]"
363
+ )
364
+ console.print(
365
+ "[dim] The starter pack attempts to create a working codebase, but you'll need to follow the generated README for complete setup.[/]"
366
+ )
356
367
  else:
357
368
  # Handle local agent selection
358
369
  agents = get_available_agents()
@@ -402,6 +413,20 @@ def create(
402
413
  temp_dir_to_clean = str(temp_dir_path)
403
414
  final_agent = f"remote_{hash(agent)}" # Generate unique name for remote template
404
415
 
416
+ # Show informational message for ADK samples with smart defaults
417
+ if remote_spec.is_adk_samples:
418
+ config = load_remote_template_config(
419
+ template_source_path, is_adk_sample=True
420
+ )
421
+ if not config.get("has_explicit_config", True):
422
+ console = Console()
423
+ console.print(
424
+ "\n[blue]ℹ️ Note: The starter pack uses heuristics to template this ADK sample agent.[/]"
425
+ )
426
+ console.print(
427
+ "[dim] The starter pack attempts to create a working codebase, but you'll need to follow the generated README for complete setup.[/]"
428
+ )
429
+
405
430
  if debug:
406
431
  logging.debug(f"Selected agent: {final_agent}")
407
432
 
@@ -414,7 +439,9 @@ def create(
414
439
 
415
440
  # Load remote template config
416
441
  source_config = load_remote_template_config(
417
- template_source_path, cli_overrides
442
+ template_source_path,
443
+ cli_overrides,
444
+ is_adk_sample=remote_spec.is_adk_samples if remote_spec else False,
418
445
  )
419
446
 
420
447
  # Remote templates now work even without pyproject.toml thanks to defaults
@@ -779,56 +806,10 @@ def display_adk_samples_selection() -> str:
779
806
  # Fetch the repository
780
807
  repo_path, _ = fetch_remote_template(spec)
781
808
 
782
- # Scan for agents in the repository
783
- adk_agents = {}
784
- agent_count = 1
785
-
786
- # Search for pyproject.toml files to identify agents
787
- for config_path in sorted(repo_path.glob("**/pyproject.toml")):
788
- try:
789
- with open(config_path, "rb") as f:
790
- pyproject_data = tomllib.load(f)
791
-
792
- config = pyproject_data.get("tool", {}).get("agent-starter-pack", {})
793
-
794
- # Skip pyproject.toml files that don't have agent-starter-pack config
795
- if not config:
796
- continue
797
-
798
- template_root = config_path.parent
799
-
800
- # Use fallbacks to [project] section if needed
801
- project_info = pyproject_data.get("project", {})
802
- agent_name = (
803
- config.get("name") or project_info.get("name") or template_root.name
804
- )
805
- description = (
806
- config.get("description") or project_info.get("description") or ""
807
- )
809
+ # Use shared ADK discovery function
810
+ from ..utils.remote_template import discover_adk_agents
808
811
 
809
- # Get the relative path from repo root
810
- relative_path = template_root.relative_to(repo_path)
811
-
812
- # For adk-samples, use just the agent name for the spec
813
- # This handles cases like python/agents/gemini-fullstack -> gemini-fullstack
814
- agent_spec_name = (
815
- relative_path.name
816
- if relative_path != relative_path.parent
817
- else str(relative_path)
818
- )
819
-
820
- adk_agents[agent_count] = {
821
- "name": agent_name,
822
- "description": description,
823
- "path": str(relative_path),
824
- "spec": f"adk@{agent_spec_name}",
825
- }
826
- agent_count += 1
827
-
828
- except Exception as e:
829
- logging.warning(
830
- f"Could not load agent from {config_path.parent.parent}: {e}"
831
- )
812
+ adk_agents = discover_adk_agents(repo_path)
832
813
 
833
814
  if not adk_agents:
834
815
  console.print("No agents found in adk-samples repository", style="yellow")
@@ -836,9 +817,19 @@ def display_adk_samples_selection() -> str:
836
817
  return display_agent_selection()
837
818
 
838
819
  console.print("\n> Available agents from [bold blue]google/adk-samples[/]:")
820
+
821
+ # Show explanation for inferred agents at the top
822
+ from ..utils.remote_template import display_adk_caveat_if_needed
823
+
824
+ display_adk_caveat_if_needed(adk_agents)
825
+
839
826
  for num, agent in adk_agents.items():
827
+ name_with_indicator = agent["name"]
828
+ if not agent.get("has_explicit_config", True):
829
+ name_with_indicator += " *"
830
+
840
831
  console.print(
841
- f"{num}. [bold]{agent['name']}[/] - [dim]{agent['description']}[/]"
832
+ f"{num}. [bold]{name_with_indicator}[/] - [dim]{agent['description']}[/]"
842
833
  )
843
834
 
844
835
  # Add option to go back to local agents
@@ -1035,18 +1026,22 @@ def _handle_credential_verification(creds_info: dict) -> dict:
1035
1026
  return creds_info
1036
1027
 
1037
1028
 
1038
- def _test_vertex_ai_connection(project_id: str, region: str) -> None:
1029
+ def _test_vertex_ai_connection(
1030
+ project_id: str, region: str, auto_approve: bool = False
1031
+ ) -> None:
1039
1032
  """Test connection to Vertex AI.
1040
1033
 
1041
1034
  Args:
1042
1035
  project_id: GCP project ID
1043
1036
  region: GCP region for deployment
1037
+ auto_approve: Whether to auto-approve API enablement
1044
1038
  """
1045
1039
  console.print("> Testing GCP and Vertex AI Connection...")
1046
1040
  try:
1047
1041
  verify_vertex_connection(
1048
1042
  project_id=project_id,
1049
1043
  location=region,
1044
+ auto_approve=auto_approve,
1050
1045
  )
1051
1046
  console.print(
1052
1047
  f"> ✓ Successfully verified connection to Vertex AI in project {project_id}"
@@ -1058,6 +1053,7 @@ def _test_vertex_ai_connection(project_id: str, region: str) -> None:
1058
1053
  f"Visit https://cloud.google.com/vertex-ai/docs/authentication for help.",
1059
1054
  style="bold red",
1060
1055
  )
1056
+ raise
1061
1057
 
1062
1058
 
1063
1059
  def replace_region_in_files(
src/cli/commands/list.py CHANGED
@@ -31,7 +31,9 @@ from ..utils.template import get_available_agents
31
31
  console = Console()
32
32
 
33
33
 
34
- def display_agents_from_path(base_path: pathlib.Path, source_name: str) -> None:
34
+ def display_agents_from_path(
35
+ base_path: pathlib.Path, source_name: str, is_adk_samples: bool = False
36
+ ) -> None:
35
37
  """Scans a directory and displays available agents."""
36
38
  table = Table(
37
39
  title=f"Available agents in [bold blue]{source_name}[/]",
@@ -47,41 +49,66 @@ def display_agents_from_path(base_path: pathlib.Path, source_name: str) -> None:
47
49
  return
48
50
 
49
51
  found_agents = False
50
- # Search for pyproject.toml files to identify agents (explicit opt-in)
51
- for config_path in sorted(base_path.glob("**/pyproject.toml")):
52
- try:
53
- with open(config_path, "rb") as f:
54
- pyproject_data = tomllib.load(f)
52
+ adk_agents = {}
55
53
 
56
- config = pyproject_data.get("tool", {}).get("agent-starter-pack", {})
54
+ if is_adk_samples:
55
+ # For ADK samples, use the shared discovery function
56
+ from ..utils.remote_template import discover_adk_agents
57
57
 
58
- # Skip pyproject.toml files that don't have agent-starter-pack config
59
- if not config:
60
- continue
58
+ adk_agents = discover_adk_agents(base_path)
61
59
 
62
- template_root = config_path.parent
60
+ for agent_info in adk_agents.values():
61
+ # Add indicator for inferred agents
62
+ name_with_indicator = agent_info["name"]
63
+ if not agent_info.get("has_explicit_config", True):
64
+ name_with_indicator += " *"
63
65
 
64
- # Use fallbacks to [project] section if needed
65
- project_info = pyproject_data.get("project", {})
66
- agent_name = (
67
- config.get("name") or project_info.get("name") or template_root.name
68
- )
69
- description = (
70
- config.get("description") or project_info.get("description") or ""
66
+ table.add_row(
67
+ name_with_indicator, f"/{agent_info['path']}", agent_info["description"]
71
68
  )
69
+ found_agents = True
70
+ else:
71
+ # Original logic for non-ADK sources: Search for pyproject.toml files with explicit config
72
+ for config_path in sorted(base_path.glob("**/pyproject.toml")):
73
+ try:
74
+ with open(config_path, "rb") as f:
75
+ pyproject_data = tomllib.load(f)
72
76
 
73
- # Display the agent's path relative to the scanned directory
74
- relative_path = template_root.relative_to(base_path)
77
+ config = pyproject_data.get("tool", {}).get("agent-starter-pack", {})
75
78
 
76
- table.add_row(agent_name, f"/{relative_path}", description)
77
- found_agents = True
79
+ # Skip pyproject.toml files that don't have agent-starter-pack config
80
+ if not config:
81
+ continue
82
+
83
+ template_root = config_path.parent
84
+
85
+ # Use fallbacks to [project] section if needed
86
+ project_info = pyproject_data.get("project", {})
87
+ agent_name = (
88
+ config.get("name") or project_info.get("name") or template_root.name
89
+ )
90
+ description = (
91
+ config.get("description") or project_info.get("description") or ""
92
+ )
93
+
94
+ # Display the agent's path relative to the scanned directory
95
+ relative_path = template_root.relative_to(base_path)
78
96
 
79
- except Exception as e:
80
- logging.warning(f"Could not load agent from {config_path.parent}: {e}")
97
+ table.add_row(agent_name, f"/{relative_path}", description)
98
+ found_agents = True
99
+
100
+ except Exception as e:
101
+ logging.warning(f"Could not load agent from {config_path.parent}: {e}")
81
102
 
82
103
  if not found_agents:
83
104
  console.print(f"No agents found in {source_name}", style="yellow")
84
105
  else:
106
+ # Show explanation for inferred agents at the top (only for ADK samples)
107
+ if is_adk_samples:
108
+ from ..utils.remote_template import display_adk_caveat_if_needed
109
+
110
+ display_adk_caveat_if_needed(adk_agents)
111
+
85
112
  console.print(table)
86
113
 
87
114
 
@@ -103,7 +130,14 @@ def list_remote_agents(remote_source: str, scan_from_root: bool = False) -> None
103
130
  repo_path, template_path = template_dir_path
104
131
  scan_path = repo_path if scan_from_root else template_path
105
132
 
106
- display_agents_from_path(scan_path, remote_source)
133
+ # Check if this is ADK samples to enable inference
134
+ is_adk_samples = (
135
+ spec.is_adk_samples if hasattr(spec, "is_adk_samples") else False
136
+ )
137
+
138
+ display_agents_from_path(
139
+ scan_path, remote_source, is_adk_samples=is_adk_samples
140
+ )
107
141
 
108
142
  except (RuntimeError, FileNotFoundError) as e:
109
143
  console.print(f"Error: {e}", style="bold red")
src/cli/utils/gcp.py CHANGED
@@ -14,9 +14,11 @@
14
14
 
15
15
  # ruff: noqa: E722
16
16
  import subprocess
17
+ import time
17
18
 
18
19
  import google.auth
19
20
  from google.api_core.client_options import ClientOptions
21
+ from google.api_core.exceptions import PermissionDenied
20
22
  from google.api_core.gapic_v1.client_info import ClientInfo
21
23
  from google.cloud.aiplatform import initializer
22
24
  from google.cloud.aiplatform_v1beta1.services.prediction_service import (
@@ -25,9 +27,94 @@ from google.cloud.aiplatform_v1beta1.services.prediction_service import (
25
27
  from google.cloud.aiplatform_v1beta1.types.prediction_service import (
26
28
  CountTokensRequest,
27
29
  )
30
+ from rich.console import Console
31
+ from rich.prompt import Confirm
28
32
 
29
33
  from src.cli.utils.version import PACKAGE_NAME, get_current_version
30
34
 
35
+ console = Console()
36
+
37
+
38
+ def enable_vertex_ai_api(project_id: str, auto_approve: bool = False) -> bool:
39
+ """Enable Vertex AI API with user confirmation and propagation waiting."""
40
+ api_name = "aiplatform.googleapis.com"
41
+
42
+ # First test if API is already working with a direct connection
43
+ if _test_vertex_ai_connection(project_id):
44
+ return True
45
+
46
+ if not auto_approve:
47
+ console.print(
48
+ f"Vertex AI API is not enabled in project '{project_id}'.", style="yellow"
49
+ )
50
+ console.print(
51
+ "To continue, we need to enable the Vertex AI API.", style="yellow"
52
+ )
53
+
54
+ if not Confirm.ask(
55
+ "Do you want to enable the Vertex AI API now?", default=True
56
+ ):
57
+ return False
58
+
59
+ try:
60
+ console.print("Enabling Vertex AI API...")
61
+ subprocess.run(
62
+ [
63
+ "gcloud",
64
+ "services",
65
+ "enable",
66
+ api_name,
67
+ "--project",
68
+ project_id,
69
+ ],
70
+ check=True,
71
+ capture_output=True,
72
+ text=True,
73
+ )
74
+ console.print("✓ Vertex AI API enabled successfully")
75
+
76
+ # Wait for API propagation
77
+ console.print("⏳ Waiting for API availability to propagate...")
78
+ max_wait_time = 180 # 3 minutes
79
+ check_interval = 10 # 10 seconds
80
+ start_time = time.time()
81
+
82
+ while time.time() - start_time < max_wait_time:
83
+ if _test_vertex_ai_connection(project_id):
84
+ console.print("✓ Vertex AI API is now available")
85
+ return True
86
+ time.sleep(check_interval)
87
+ console.print("⏳ Still waiting for API propagation...")
88
+
89
+ console.print(
90
+ "⚠️ API propagation took longer than expected, but continuing...",
91
+ style="yellow",
92
+ )
93
+ return True
94
+
95
+ except subprocess.CalledProcessError as e:
96
+ console.print(f"Failed to enable Vertex AI API: {e.stderr}", style="bold red")
97
+ return False
98
+
99
+
100
+ def _test_vertex_ai_connection(project_id: str, location: str = "us-central1") -> bool:
101
+ """Test Vertex AI connection without raising exceptions."""
102
+ try:
103
+ credentials, _ = google.auth.default()
104
+ client = PredictionServiceClient(
105
+ credentials=credentials,
106
+ client_options=ClientOptions(
107
+ api_endpoint=f"{location}-aiplatform.googleapis.com"
108
+ ),
109
+ client_info=get_client_info(),
110
+ transport=initializer.global_config._api_transport,
111
+ )
112
+ request = get_dummy_request(project_id=project_id)
113
+ client.count_tokens(request=request)
114
+ return True
115
+ except Exception:
116
+ return False
117
+
31
118
 
32
119
  def get_user_agent() -> str:
33
120
  """Returns custom user agent header tuple (version, agent string)."""
@@ -52,8 +139,18 @@ def get_dummy_request(project_id: str) -> CountTokensRequest:
52
139
  def verify_vertex_connection(
53
140
  project_id: str,
54
141
  location: str = "us-central1",
142
+ auto_approve: bool = False,
55
143
  ) -> None:
56
144
  """Verifies Vertex AI connection with a test Gemini request."""
145
+ # First try direct connection - if it works, we're done
146
+ if _test_vertex_ai_connection(project_id, location):
147
+ return
148
+
149
+ # If that failed, try to enable the API
150
+ if not enable_vertex_ai_api(project_id, auto_approve):
151
+ raise Exception("Vertex AI API is not enabled and user declined to enable it")
152
+
153
+ # After enabling, test again with proper error handling
57
154
  credentials, _ = google.auth.default()
58
155
  client = PredictionServiceClient(
59
156
  credentials=credentials,
@@ -64,7 +161,31 @@ def verify_vertex_connection(
64
161
  transport=initializer.global_config._api_transport,
65
162
  )
66
163
  request = get_dummy_request(project_id=project_id)
67
- client.count_tokens(request=request)
164
+
165
+ try:
166
+ client.count_tokens(request=request)
167
+ except PermissionDenied as e:
168
+ error_message = str(e)
169
+ # Check if the error is specifically about API not being enabled
170
+ if (
171
+ "has not been used" in error_message
172
+ and "aiplatform.googleapis.com" in error_message
173
+ ):
174
+ # This shouldn't happen since we checked above, but handle it gracefully
175
+ console.print(
176
+ "⚠️ API may still be propagating, retrying in 30 seconds...",
177
+ style="yellow",
178
+ )
179
+ time.sleep(30)
180
+ try:
181
+ client.count_tokens(request=request)
182
+ except PermissionDenied:
183
+ raise Exception(
184
+ "Vertex AI API is enabled but not yet available. Please wait a few more minutes and try again."
185
+ ) from e
186
+ else:
187
+ # Re-raise other permission errors
188
+ raise
68
189
 
69
190
 
70
191
  def verify_credentials() -> dict:
@@ -28,6 +28,7 @@ if sys.version_info >= (3, 11):
28
28
  else:
29
29
  import tomli as tomllib
30
30
  from jinja2 import Environment
31
+ from rich.console import Console
31
32
 
32
33
 
33
34
  @dataclass
@@ -94,10 +95,14 @@ def parse_agent_spec(agent_spec: str) -> RemoteTemplateSpec | None:
94
95
  template_path = path_parts[0]
95
96
  git_ref = path_parts[1]
96
97
 
98
+ # Check if this is the ADK samples repository
99
+ is_adk_samples = repo_url == "https://github.com/google/adk-samples"
100
+
97
101
  return RemoteTemplateSpec(
98
102
  repo_url=repo_url,
99
103
  template_path=template_path.strip("/"),
100
104
  git_ref=git_ref,
105
+ is_adk_samples=is_adk_samples,
101
106
  )
102
107
 
103
108
  # GitHub shorthand: <org>/<repo>[/<path>][@<ref>]
@@ -108,10 +113,15 @@ def parse_agent_spec(agent_spec: str) -> RemoteTemplateSpec | None:
108
113
  repo = match.group(2)
109
114
  template_path = match.group(3) or ""
110
115
  git_ref = match.group(4) or "main"
116
+
117
+ # Check if this is the ADK samples repository
118
+ is_adk_samples = org == "google" and repo == "adk-samples"
119
+
111
120
  return RemoteTemplateSpec(
112
121
  repo_url=f"https://github.com/{org}/{repo}",
113
122
  template_path=template_path,
114
123
  git_ref=git_ref,
124
+ is_adk_samples=is_adk_samples,
115
125
  )
116
126
 
117
127
  return None
@@ -187,29 +197,66 @@ def fetch_remote_template(
187
197
  ) from e
188
198
 
189
199
 
200
+ def _infer_agent_directory_for_adk(
201
+ template_dir: pathlib.Path, is_adk_sample: bool
202
+ ) -> dict[str, Any]:
203
+ """Infer agent configuration for ADK samples only using Python conventions.
204
+
205
+ Args:
206
+ template_dir: Path to template directory
207
+ is_adk_sample: Whether this is an ADK sample
208
+
209
+ Returns:
210
+ Dictionary with inferred configuration, or empty dict if not ADK sample
211
+ """
212
+ if not is_adk_sample:
213
+ return {}
214
+
215
+ # Convert folder name to Python package convention (hyphens to underscores)
216
+ folder_name = template_dir.name
217
+ agent_directory = folder_name.replace("-", "_")
218
+
219
+ logging.debug(
220
+ f"Inferred agent_directory '{agent_directory}' from folder name '{folder_name}' for ADK sample"
221
+ )
222
+
223
+ return {
224
+ "settings": {
225
+ "agent_directory": agent_directory,
226
+ },
227
+ "has_explicit_config": False, # Track that this was inferred
228
+ }
229
+
230
+
190
231
  def load_remote_template_config(
191
- template_dir: pathlib.Path, cli_overrides: dict[str, Any] | None = None
232
+ template_dir: pathlib.Path,
233
+ cli_overrides: dict[str, Any] | None = None,
234
+ is_adk_sample: bool = False,
192
235
  ) -> dict[str, Any]:
193
236
  """Load template configuration from remote template's pyproject.toml with CLI overrides.
194
237
 
195
238
  Loads configuration from [tool.agent-starter-pack] section with fallbacks
196
239
  to [project] section for name and description if not specified. CLI overrides
197
- take precedence over all other sources.
240
+ take precedence over all other sources. For ADK samples without explicit config,
241
+ uses smart inference for agent directory naming.
198
242
 
199
243
  Args:
200
244
  template_dir: Path to template directory
201
245
  cli_overrides: Configuration overrides from CLI (takes highest precedence)
246
+ is_adk_sample: Whether this is an ADK sample (enables smart inference)
202
247
 
203
248
  Returns:
204
249
  Template configuration dictionary with merged sources
205
250
  """
206
- config = {}
251
+ config: dict[str, Any] = {}
252
+ has_explicit_config = False
207
253
 
208
254
  # Start with defaults
209
255
  defaults = {
210
256
  "base_template": "adk_base",
211
257
  "name": template_dir.name,
212
258
  "description": "",
259
+ "agent_directory": "app", # Default for non-ADK samples
213
260
  }
214
261
  config.update(defaults)
215
262
 
@@ -226,9 +273,13 @@ def load_remote_template_config(
226
273
  # Fallback to [project] fields if not specified in agent-starter-pack section
227
274
  project_info = pyproject_data.get("project", {})
228
275
 
276
+ # Track if we have explicit configuration
277
+ has_explicit_config = bool(toml_config)
278
+
229
279
  # Apply pyproject.toml configuration (overrides defaults)
230
280
  if toml_config:
231
281
  config.update(toml_config)
282
+ logging.debug("Found explicit [tool.agent-starter-pack] configuration")
232
283
 
233
284
  # Apply [project] fallbacks if not already set
234
285
  if "name" not in toml_config and "name" in project_info:
@@ -240,6 +291,31 @@ def load_remote_template_config(
240
291
  logging.debug(f"Loaded template config from {pyproject_path}")
241
292
  except Exception as e:
242
293
  logging.error(f"Error loading pyproject.toml config: {e}")
294
+ else:
295
+ # No pyproject.toml found
296
+ if is_adk_sample:
297
+ logging.debug(
298
+ f"No pyproject.toml found for ADK sample {template_dir.name}, will use inference"
299
+ )
300
+ else:
301
+ logging.debug(
302
+ f"No pyproject.toml found for template {template_dir.name}, using defaults"
303
+ )
304
+
305
+ # Apply ADK inference if no explicit config and this is an ADK sample
306
+ if not has_explicit_config and is_adk_sample:
307
+ try:
308
+ inferred_config = _infer_agent_directory_for_adk(
309
+ template_dir, is_adk_sample
310
+ )
311
+ config.update(inferred_config)
312
+ logging.debug("Applied ADK inference for template without explicit config")
313
+ except Exception as e:
314
+ logging.warning(f"Failed to apply ADK inference for {template_dir}: {e}")
315
+ # Continue with default configuration
316
+
317
+ # Add metadata about configuration source
318
+ config["has_explicit_config"] = bool(has_explicit_config)
243
319
 
244
320
  # Apply CLI overrides (highest precedence) using deep merge
245
321
  if cli_overrides:
@@ -291,6 +367,97 @@ def merge_template_configs(
291
367
  return deep_merge(merged_config, remote_config)
292
368
 
293
369
 
370
+ def discover_adk_agents(repo_path: pathlib.Path) -> dict[int, dict[str, Any]]:
371
+ """Discover and load all ADK agents from a repository with inference support.
372
+
373
+ Args:
374
+ repo_path: Path to the cloned ADK samples repository
375
+
376
+ Returns:
377
+ Dictionary mapping agent numbers to agent info with keys:
378
+ - name: Agent display name
379
+ - description: Agent description
380
+ - path: Relative path from repo root
381
+ - spec: adk@ specification string
382
+ - has_explicit_config: Whether agent has explicit configuration
383
+ """
384
+ import logging
385
+
386
+ adk_agents = {}
387
+
388
+ # Search specifically for agents in python/agents/* directories
389
+ agents_dir = repo_path / "python" / "agents"
390
+ logging.debug(f"Looking for agents in: {agents_dir}")
391
+ if agents_dir.exists():
392
+ all_items = list(agents_dir.iterdir())
393
+ logging.debug(
394
+ f"Found items in agents directory: {[item.name for item in all_items]}"
395
+ )
396
+
397
+ # Collect all agents first, then sort by configuration type
398
+ all_agents = []
399
+
400
+ for agent_dir in sorted(agents_dir.iterdir()):
401
+ if not agent_dir.is_dir():
402
+ logging.debug(f"Skipping non-directory: {agent_dir.name}")
403
+ continue
404
+ logging.debug(f"Processing agent directory: {agent_dir.name}")
405
+
406
+ try:
407
+ # Load configuration with ADK inference support
408
+ config = load_remote_template_config(
409
+ template_dir=agent_dir, is_adk_sample=True
410
+ )
411
+
412
+ agent_name = config.get("name", agent_dir.name)
413
+ description = config.get("description", "")
414
+ has_explicit_config = config.get("has_explicit_config", False)
415
+
416
+ # Get the relative path from repo root
417
+ relative_path = agent_dir.relative_to(repo_path)
418
+ agent_spec_name = agent_dir.name
419
+
420
+ agent_info = {
421
+ "name": agent_name,
422
+ "description": description,
423
+ "path": str(relative_path),
424
+ "spec": f"adk@{agent_spec_name}",
425
+ "has_explicit_config": has_explicit_config,
426
+ }
427
+ all_agents.append(agent_info)
428
+
429
+ except Exception as e:
430
+ logging.warning(f"Could not load agent from {agent_dir}: {e}")
431
+
432
+ # Sort agents: explicit config first, then inferred (both alphabetically within their groups)
433
+ all_agents.sort(key=lambda x: (not x["has_explicit_config"], x["name"].lower()))
434
+
435
+ # Convert to numbered dictionary
436
+ for i, agent_info in enumerate(all_agents, 1):
437
+ adk_agents[i] = agent_info
438
+
439
+ return adk_agents
440
+
441
+
442
+ def display_adk_caveat_if_needed(agents: dict[int, dict[str, Any]]) -> None:
443
+ """Display helpful note for agents that use inference.
444
+
445
+ Args:
446
+ agents: Dictionary of agent info from discover_adk_agents
447
+ """
448
+ console = Console()
449
+ inferred_agents = [
450
+ a for a in agents.values() if not a.get("has_explicit_config", True)
451
+ ]
452
+ if inferred_agents:
453
+ console.print(
454
+ "\n[blue]ℹ️ Note: Agents marked with * are templated using starter pack heuristics for ADK samples.[/]"
455
+ )
456
+ console.print(
457
+ "[dim] The starter pack attempts to create a working codebase, but you'll need to follow the generated README for complete setup.[/]"
458
+ )
459
+
460
+
294
461
  def render_and_merge_makefiles(
295
462
  base_template_path: pathlib.Path,
296
463
  final_destination: pathlib.Path,
src/cli/utils/template.py CHANGED
@@ -686,7 +686,7 @@ def process_template(
686
686
  llm_txt_content = f.read()
687
687
 
688
688
  cookiecutter_config = {
689
- "project_name": "my-project",
689
+ "project_name": project_name,
690
690
  "agent_name": agent_name,
691
691
  "package_version": get_current_version(),
692
692
  "agent_description": template_config.get("description", ""),
@@ -752,6 +752,53 @@ def process_template(
752
752
  logging.debug(
753
753
  f"Copying remote template files from {remote_template_path} to {generated_project_dir}"
754
754
  )
755
+
756
+ # Preserve base template README and pyproject.toml files before overwriting
757
+ preserve_files = ["README.md"]
758
+
759
+ # Only preserve pyproject.toml if the remote template doesn't have starter pack integration
760
+ remote_pyproject = remote_template_path / "pyproject.toml"
761
+ if remote_pyproject.exists():
762
+ try:
763
+ remote_pyproject_content = remote_pyproject.read_text()
764
+ # Check for starter pack integration markers
765
+ has_starter_pack_integration = (
766
+ "[tool.agent-starter-pack]" in remote_pyproject_content
767
+ )
768
+ if not has_starter_pack_integration:
769
+ preserve_files.append("pyproject.toml")
770
+ logging.debug(
771
+ "Remote pyproject.toml lacks starter pack integration - will preserve base template version"
772
+ )
773
+ else:
774
+ logging.debug(
775
+ "Remote pyproject.toml has starter pack integration - using remote version only"
776
+ )
777
+ except Exception as e:
778
+ logging.warning(
779
+ f"Could not read remote pyproject.toml: {e}. Will preserve base template version."
780
+ )
781
+ preserve_files.append("pyproject.toml")
782
+ else:
783
+ preserve_files.append("pyproject.toml")
784
+
785
+ for preserve_file in preserve_files:
786
+ base_file = generated_project_dir / preserve_file
787
+ remote_file = remote_template_path / preserve_file
788
+
789
+ if base_file.exists() and remote_file.exists():
790
+ # Preserve the base template file with starter_pack prefix
791
+ base_name = pathlib.Path(preserve_file).stem
792
+ extension = pathlib.Path(preserve_file).suffix
793
+ preserved_file = (
794
+ generated_project_dir
795
+ / f"starter_pack_{base_name}{extension}"
796
+ )
797
+ shutil.copy2(base_file, preserved_file)
798
+ logging.debug(
799
+ f"Preserved base template {preserve_file} as starter_pack_{base_name}{extension}"
800
+ )
801
+
755
802
  copy_files(
756
803
  remote_template_path,
757
804
  generated_project_dir,
@@ -884,11 +931,35 @@ def process_template(
884
931
  )
885
932
 
886
933
  if generated_project_dir.exists():
934
+ # Check for existing README and pyproject.toml files before removing destination
935
+ existing_preserved_files = []
887
936
  if final_destination.exists():
937
+ for item in final_destination.iterdir():
938
+ if item.is_file() and (
939
+ item.name.lower().startswith("readme")
940
+ or item.name == "pyproject.toml"
941
+ ):
942
+ existing_preserved_files.append(
943
+ (item.name, item.read_text())
944
+ )
888
945
  shutil.rmtree(final_destination)
946
+
889
947
  shutil.copytree(
890
948
  generated_project_dir, final_destination, dirs_exist_ok=True
891
949
  )
950
+
951
+ # Restore existing README and pyproject.toml files with starter_pack prefix
952
+ for file_name, file_content in existing_preserved_files:
953
+ base_name = pathlib.Path(file_name).stem
954
+ extension = pathlib.Path(file_name).suffix
955
+ preserved_file_path = (
956
+ final_destination / f"starter_pack_{base_name}{extension}"
957
+ )
958
+ preserved_file_path.write_text(file_content)
959
+ logging.debug(
960
+ f"File preservation: existing {file_name} preserved as starter_pack_{base_name}{extension}"
961
+ )
962
+
892
963
  logging.debug(
893
964
  f"Project successfully created at {final_destination}"
894
965
  )
@@ -16,12 +16,28 @@ FROM python:3.11-slim
16
16
 
17
17
  RUN pip install --no-cache-dir uv==0.6.12
18
18
 
19
+ {%- if cookiecutter.agent_name == 'live_api' %}
20
+ # Install Node.js for frontend build
21
+ RUN apt-get update && apt-get install -y \
22
+ curl \
23
+ && curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
24
+ && apt-get install -y nodejs \
25
+ && apt-get clean \
26
+ && rm -rf /var/lib/apt/lists/*
27
+ {%- endif %}
28
+
19
29
  WORKDIR /code
20
30
 
21
31
  COPY ./pyproject.toml ./README.md ./uv.lock* ./
22
32
 
23
33
  COPY ./{{cookiecutter.agent_directory}} ./{{cookiecutter.agent_directory}}
24
34
 
35
+ {%- if cookiecutter.agent_name == 'live_api' %}
36
+ # Copy and build frontend
37
+ COPY ./frontend ./frontend
38
+ RUN cd frontend && npm ci && npm run build
39
+ {%- endif %}
40
+
25
41
  RUN uv sync --frozen
26
42
 
27
43
  ARG COMMIT_SHA=""
@@ -16,11 +16,14 @@ import asyncio
16
16
  import json
17
17
  import logging
18
18
  from collections.abc import Callable
19
+ from pathlib import Path
19
20
  from typing import Any, Literal
20
21
 
21
22
  import backoff
22
- from fastapi import FastAPI, WebSocket
23
+ from fastapi import FastAPI, HTTPException, WebSocket
23
24
  from fastapi.middleware.cors import CORSMiddleware
25
+ from fastapi.responses import FileResponse
26
+ from fastapi.staticfiles import StaticFiles
24
27
  from google.cloud import logging as google_cloud_logging
25
28
  from google.genai import types
26
29
  from google.genai.types import LiveServerToolCall
@@ -36,6 +39,18 @@ app.add_middleware(
36
39
  allow_methods=["*"],
37
40
  allow_headers=["*"],
38
41
  )
42
+
43
+ # Get the path to the frontend build directory
44
+ current_dir = Path(__file__).parent
45
+ frontend_build_dir = current_dir.parent / "frontend" / "build"
46
+
47
+ # Mount static files if build directory exists
48
+ if frontend_build_dir.exists():
49
+ app.mount(
50
+ "/static",
51
+ StaticFiles(directory=str(frontend_build_dir / "static")),
52
+ name="static",
53
+ )
39
54
  logging_client = google_cloud_logging.Client()
40
55
  logger = logging_client.logger(__name__)
41
56
  logging.basicConfig(level=logging.INFO)
@@ -382,8 +397,41 @@ def collect_feedback(feedback: Feedback) -> dict[str, str]:
382
397
  """
383
398
  logger.log_struct(feedback.model_dump(), severity="INFO")
384
399
  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
+ )
385
412
 
386
413
 
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
+
387
435
  # Main execution
388
436
  if __name__ == "__main__":
389
437
  import uvicorn
@@ -21,8 +21,9 @@ import SidePanel from "./components/side-panel/SidePanel";
21
21
  import ControlTray from "./components/control-tray/ControlTray";
22
22
  import cn from "classnames";
23
23
 
24
- const defaultHost = "localhost:8000";
25
- const defaultUri = `ws://${defaultHost}/`;
24
+ // Use relative URLs that work with integrated setup and deployments
25
+ const defaultHost = window.location.host;
26
+ const defaultUri = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${defaultHost}/`;
26
27
 
27
28
  function App() {
28
29
  const videoRef = useRef<HTMLVideoElement>(null);
@@ -74,7 +74,9 @@ export class MultimodalLiveClient extends EventEmitter<MultimodalLiveClientEvent
74
74
  private userId?: string;
75
75
  constructor({ url, userId, runId }: MultimodalLiveAPIClientConnection) {
76
76
  super();
77
- url = url || `ws://localhost:8000/ws`;
77
+ // Use relative URL that works with integrated setup and deployments
78
+ const defaultWsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
79
+ url = url || defaultWsUrl;
78
80
  this.url = new URL("ws", url).href;
79
81
  this.userId = userId;
80
82
  this.runId = runId || crypto.randomUUID(); // Ensure runId is always a string by providing default