|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 | # Builds the release candidate and checks it does not import worse than the |
16 | | -# last published release. Publishing otherwise never installs the wheel it is |
17 | | -# about to upload. |
| 16 | +# last published release, then walks the quickstart against it. Publishing |
| 17 | +# otherwise never installs the wheel it is about to upload. |
18 | 18 | # |
19 | 19 | # This runs on the release pull request, which is where the version bump and |
20 | 20 | # the changelog live and where the release oncaller is already looking. It is |
@@ -129,3 +129,184 @@ jobs: |
129 | 129 | fi |
130 | 130 | gh pr comment "$PR_NUMBER" --body-file release-artifact-check.md --edit-last \ |
131 | 131 | || gh pr comment "$PR_NUMBER" --body-file release-artifact-check.md |
| 132 | +
|
| 133 | + # Does what the quickstart tells a new user to do, against a wheel built from |
| 134 | + # the release candidate: create an agent, chat with it, serve it with the API |
| 135 | + # server that Cloud Run, GKE and Docker deploys run, and evaluate it. Unit |
| 136 | + # tests run on locked dependencies; this installs the wheel fresh, the way a |
| 137 | + # user does, so a dependency release that breaks ADK shows up here instead of |
| 138 | + # on a first run. |
| 139 | + quickstart-smoke: |
| 140 | + if: github.repository == 'google/adk-python' |
| 141 | + runs-on: ubuntu-latest |
| 142 | + timeout-minutes: 30 |
| 143 | + permissions: |
| 144 | + contents: read |
| 145 | + |
| 146 | + steps: |
| 147 | + - name: Checkout candidate |
| 148 | + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 |
| 149 | + |
| 150 | + - name: Install uv |
| 151 | + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 |
| 152 | + with: |
| 153 | + version: "latest" |
| 154 | + enable-cache: true |
| 155 | + |
| 156 | + - name: Set up Python |
| 157 | + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 |
| 158 | + with: |
| 159 | + python-version: "3.11" |
| 160 | + |
| 161 | + - name: Build distributions |
| 162 | + run: uv build |
| 163 | + |
| 164 | + # A pull request from a fork gets no secrets. Fail with a reason instead |
| 165 | + # of letting adk create stop at a prompt nobody can answer. |
| 166 | + - name: Check for a model key |
| 167 | + env: |
| 168 | + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} |
| 169 | + run: | |
| 170 | + if [[ -z "${GOOGLE_API_KEY:-}" ]]; then |
| 171 | + echo "::error::GOOGLE_API_KEY is not available to this run, so the quickstart cannot call a model." |
| 172 | + exit 1 |
| 173 | + fi |
| 174 | +
|
| 175 | + # A plain install, as the quickstart does. The eval extra comes later, so |
| 176 | + # a core path that leans on an eval-only package fails here. |
| 177 | + - name: Install the candidate wheel into a fresh environment |
| 178 | + run: | |
| 179 | + set -euo pipefail |
| 180 | + uv venv "$RUNNER_TEMP/quickstart-venv" |
| 181 | + uv pip install --python "$RUNNER_TEMP/quickstart-venv/bin/python" dist/*.whl |
| 182 | + echo "$RUNNER_TEMP/quickstart-venv/bin" >> "$GITHUB_PATH" |
| 183 | + mkdir -p "$RUNNER_TEMP/agents" |
| 184 | +
|
| 185 | + # Picks option 1 at the model prompt, the model adk create offers first, |
| 186 | + # so a default that stopped working fails here before a new user hits it. |
| 187 | + # adk create writes the key into the agent's .env, which the later steps |
| 188 | + # load, so only this step needs the secret. |
| 189 | + - name: Create an agent |
| 190 | + working-directory: ${{ runner.temp }}/agents |
| 191 | + env: |
| 192 | + GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} |
| 193 | + run: printf '1\n' | adk create smoke_agent --api_key "$GOOGLE_API_KEY" |
| 194 | + |
| 195 | + # The same interactive loop `adk run smoke_agent` opens for a user. The |
| 196 | + # reply prints after the `[user]: ` prompt on the same line, so the grep |
| 197 | + # is not anchored. |
| 198 | + - name: Chat with it in the terminal |
| 199 | + working-directory: ${{ runner.temp }}/agents |
| 200 | + run: | |
| 201 | + set -euo pipefail |
| 202 | + printf 'Reply with one short greeting.\nexit\n' | adk run smoke_agent | tee "$RUNNER_TEMP/run.log" |
| 203 | + grep -q '\[root_agent\]: ' "$RUNNER_TEMP/run.log" |
| 204 | +
|
| 205 | + - name: Serve it and send it a message |
| 206 | + working-directory: ${{ runner.temp }}/agents |
| 207 | + run: | |
| 208 | + set -euo pipefail |
| 209 | + adk api_server . --port 8765 > "$RUNNER_TEMP/server.log" 2>&1 & |
| 210 | + server=$! |
| 211 | + trap 'status=$?; kill "$server" 2>/dev/null || true; if [[ $status -ne 0 ]]; then cat "$RUNNER_TEMP/server.log"; fi' EXIT |
| 212 | + python - <<'EOF' |
| 213 | + import json |
| 214 | + import time |
| 215 | + import urllib.request |
| 216 | +
|
| 217 | + def call(method, path, body=None): |
| 218 | + request = urllib.request.Request( |
| 219 | + "http://127.0.0.1:8765" + path, |
| 220 | + data=None if body is None else json.dumps(body).encode(), |
| 221 | + method=method, |
| 222 | + headers={"Content-Type": "application/json"}, |
| 223 | + ) |
| 224 | + # The model call can stall for minutes on a 503 before its retry |
| 225 | + # succeeds, so allow more than one retry cycle. |
| 226 | + with urllib.request.urlopen(request, timeout=300) as response: |
| 227 | + return json.load(response) |
| 228 | +
|
| 229 | + last_error = None |
| 230 | + for _ in range(60): |
| 231 | + try: |
| 232 | + apps = call("GET", "/list-apps") |
| 233 | + break |
| 234 | + except OSError as error: |
| 235 | + last_error = error |
| 236 | + time.sleep(2) |
| 237 | + else: |
| 238 | + raise SystemExit(f"api_server never answered /list-apps: {last_error}") |
| 239 | + assert "smoke_agent" in apps, apps |
| 240 | +
|
| 241 | + session = call("POST", "/apps/smoke_agent/users/user/sessions", {}) |
| 242 | + events = call("POST", "/run", { |
| 243 | + "app_name": "smoke_agent", |
| 244 | + "user_id": "user", |
| 245 | + "session_id": session["id"], |
| 246 | + "new_message": { |
| 247 | + "role": "user", |
| 248 | + "parts": [{"text": "Reply with one short greeting."}], |
| 249 | + }, |
| 250 | + }) |
| 251 | + reply = "".join( |
| 252 | + part.get("text") or "" |
| 253 | + for event in events |
| 254 | + if event.get("author") == "root_agent" |
| 255 | + for part in (event.get("content") or {}).get("parts", []) |
| 256 | + ) |
| 257 | + assert reply.strip(), events |
| 258 | + print("root_agent replied:", reply) |
| 259 | + EOF |
| 260 | +
|
| 261 | + - name: Add the eval extra |
| 262 | + run: | |
| 263 | + set -euo pipefail |
| 264 | + wheel=$(ls dist/*.whl) |
| 265 | + uv pip install --python "$RUNNER_TEMP/quickstart-venv/bin/python" "${wheel}[eval]" |
| 266 | +
|
| 267 | + # adk eval exits 0 even when a case fails, so the summary is the verdict. |
| 268 | + # The agent has no tools, so the trajectory check is exact; the response |
| 269 | + # check only has to compute, because the model's wording varies. |
| 270 | + - name: Evaluate it |
| 271 | + working-directory: ${{ runner.temp }}/agents |
| 272 | + run: | |
| 273 | + set -euo pipefail |
| 274 | + cat > "$RUNNER_TEMP/smoke.evalset.json" <<'EOF' |
| 275 | + { |
| 276 | + "eval_set_id": "quickstart_smoke", |
| 277 | + "eval_cases": [ |
| 278 | + { |
| 279 | + "eval_id": "greeting", |
| 280 | + "conversation": [ |
| 281 | + { |
| 282 | + "invocation_id": "greeting-1", |
| 283 | + "user_content": { |
| 284 | + "role": "user", |
| 285 | + "parts": [{"text": "Reply with one short greeting."}] |
| 286 | + }, |
| 287 | + "final_response": { |
| 288 | + "role": "model", |
| 289 | + "parts": [{"text": "Hello!"}] |
| 290 | + }, |
| 291 | + "intermediate_data": { |
| 292 | + "tool_uses": [], |
| 293 | + "intermediate_responses": [] |
| 294 | + } |
| 295 | + } |
| 296 | + ], |
| 297 | + "session_input": { |
| 298 | + "app_name": "smoke_agent", |
| 299 | + "user_id": "user", |
| 300 | + "state": {} |
| 301 | + } |
| 302 | + } |
| 303 | + ] |
| 304 | + } |
| 305 | + EOF |
| 306 | + cat > "$RUNNER_TEMP/eval_config.json" <<'EOF' |
| 307 | + {"criteria": {"tool_trajectory_avg_score": 1.0, "response_match_score": 0.0}} |
| 308 | + EOF |
| 309 | + adk eval smoke_agent "$RUNNER_TEMP/smoke.evalset.json" \ |
| 310 | + --config_file_path "$RUNNER_TEMP/eval_config.json" | tee "$RUNNER_TEMP/eval.log" |
| 311 | + grep -q 'Tests passed: 1$' "$RUNNER_TEMP/eval.log" |
| 312 | + grep -q 'Tests failed: 0$' "$RUNNER_TEMP/eval.log" |
0 commit comments