agent-starter-pack 0.12.1__py3-none-any.whl → 0.13.1__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.
- {agent_starter_pack-0.12.1.dist-info → agent_starter_pack-0.13.1.dist-info}/METADATA +1 -1
- {agent_starter_pack-0.12.1.dist-info → agent_starter_pack-0.13.1.dist-info}/RECORD +16 -16
- src/base_template/Makefile +19 -7
- src/base_template/README.md +13 -48
- src/cli/commands/create.py +54 -58
- src/cli/commands/list.py +59 -25
- src/cli/utils/gcp.py +122 -1
- src/cli/utils/remote_template.py +170 -3
- src/cli/utils/template.py +72 -1
- src/deployment_targets/cloud_run/Dockerfile +16 -0
- src/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +49 -1
- src/frontends/live_api_react/frontend/src/App.tsx +3 -2
- src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +3 -1
- {agent_starter_pack-0.12.1.dist-info → agent_starter_pack-0.13.1.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.12.1.dist-info → agent_starter_pack-0.13.1.dist-info}/entry_points.txt +0 -0
- {agent_starter_pack-0.12.1.dist-info → agent_starter_pack-0.13.1.dist-info}/licenses/LICENSE +0 -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=
|
42
|
-
src/base_template/README.md,sha256=
|
41
|
+
src/base_template/Makefile,sha256=FU8E3WSC5BYznsihgZfAWO_N_7BKFoVLEX9Thg5t9aA,6834
|
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=
|
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=
|
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=
|
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=
|
85
|
-
src/cli/utils/template.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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.
|
174
|
-
agent_starter_pack-0.
|
175
|
-
agent_starter_pack-0.
|
176
|
-
agent_starter_pack-0.
|
177
|
-
agent_starter_pack-0.
|
173
|
+
agent_starter_pack-0.13.1.dist-info/METADATA,sha256=q4uaDSE42J30u5miXspxIqjRMkbUBhcaHJhTyzF-TQY,11155
|
174
|
+
agent_starter_pack-0.13.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
175
|
+
agent_starter_pack-0.13.1.dist-info/entry_points.txt,sha256=U7uCxR7YulIhZ0L8R8Hui0Bsy6J7oyESBeDYJYMrQjA,56
|
176
|
+
agent_starter_pack-0.13.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
177
|
+
agent_starter_pack-0.13.1.dist-info/RECORD,,
|
src/base_template/Makefile
CHANGED
@@ -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 %}
|
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 "| |"
|
@@ -38,14 +47,16 @@ playground:
|
|
38
47
|
{%- endif %}
|
39
48
|
@echo "==============================================================================="
|
40
49
|
{%- if "adk" in cookiecutter.tags %}
|
41
|
-
uv run adk web . --port 8501
|
50
|
+
uv run adk web . --port 8501 --reload_agents
|
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
|
-
|
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
|
src/base_template/README.md
CHANGED
@@ -92,12 +92,12 @@ Here’s the recommended workflow for local development:
|
|
92
92
|
make install
|
93
93
|
```
|
94
94
|
|
95
|
-
2. **Start the
|
96
|
-
|
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
|
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
|
-
|
117
|
-
|
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
|
123
|
+
This launches the Streamlit application, which connects to the backend server at `http://localhost:8000`.
|
124
|
+
</details>
|
125
|
+
<br>
|
122
126
|
|
123
|
-
|
124
|
-
* Open
|
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
|
-
**
|
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.
|
src/cli/commands/create.py
CHANGED
@@ -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,
|
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
|
-
#
|
783
|
-
|
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
|
-
|
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]{
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
59
|
-
if not config:
|
60
|
-
continue
|
58
|
+
adk_agents = discover_adk_agents(base_path)
|
61
59
|
|
62
|
-
|
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
|
-
|
65
|
-
|
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
|
-
|
74
|
-
relative_path = template_root.relative_to(base_path)
|
77
|
+
config = pyproject_data.get("tool", {}).get("agent-starter-pack", {})
|
75
78
|
|
76
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
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:
|
src/cli/utils/remote_template.py
CHANGED
@@ -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,
|
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":
|
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
|
-
|
25
|
-
const
|
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
|
-
|
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
|
File without changes
|
{agent_starter_pack-0.12.1.dist-info → agent_starter_pack-0.13.1.dist-info}/entry_points.txt
RENAMED
File without changes
|
{agent_starter_pack-0.12.1.dist-info → agent_starter_pack-0.13.1.dist-info}/licenses/LICENSE
RENAMED
File without changes
|