Skip to content

Commit 1782cca

Browse files
Merge branch 'main' into enforce-tool-confirmation-denials
2 parents 399e1ce + 044a1ec commit 1782cca

408 files changed

Lines changed: 41147 additions & 9037 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/skills/adk-unit-guide/SKILL.md‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,67 @@ Then add the guide to `docs/guides/README.md` under the right category heading,
118118
as `* [Title](path/index.md) - one-line summary.` That index is the only table
119119
of contents; a guide missing from it is unreachable.
120120

121+
## When a guide is not required
122+
123+
`scripts/check_new_py_files.py` enforces this convention on the Python files a
124+
change adds under `src/google/adk/`. Some it passes over without asking, and
125+
the rest you can waive, so work out which case you are in before writing a
126+
guide the rule does not ask for.
127+
128+
Three subtrees sit outside the rule, because nothing in them is published:
129+
`src/google/adk/internal/`, `src/google/adk/platform/internal/`, and
130+
`src/google/adk/v1/`.
131+
132+
Elsewhere a file is exempt on one of two grounds. It carries no unit of its
133+
own, which covers `__init__.py` and any path with a `cli/` or `utils/`
134+
component. Or it holds declarations that belong in the guide of whatever uses
135+
them rather than in a guide of their own, which covers any filename ending
136+
`_utils.py`, `_helper.py`, `_helpers.py`, `_types.py`, `_errors.py`,
137+
`_exceptions.py`, or `_constants.py`.
138+
139+
Everything else needs a guide or a waiver. A waiver is a `NO_UNIT_GUIDE=` or
140+
`SKIP_UNIT_GUIDE=` tag carrying the reason, and the two names behave
141+
identically. It waives the unit guide requirement alone; the `_` prefix rule
142+
shares the same hook and the same error report, and has no waiver at all. One
143+
tag waives every file the change adds, not the one its reason names, so a
144+
change adding several needs a reason that covers all of them.
145+
146+
Write a reason a reviewer can check against the code, because the tag becomes
147+
the only record of why the guide is absent:
148+
149+
```
150+
NO_UNIT_GUIDE=_model_call is internal to the LLM flow; no __init__.py re-exports it.
151+
```
152+
153+
### Where to put the waiver
154+
155+
The tag reaches the check through a commit message or through the environment,
156+
and which one applies depends on whether the commit exists yet.
157+
158+
Once the commit exists, its message carries the tag. Continuous integration
159+
reads the messages of the commits a pull request contains, so a waiver a
160+
contributor writes there satisfies the check that gates the pull request. Start
161+
the tag at the left margin with no space before the `=`, because the check
162+
matches at the start of a line. An indented tag, or one inside a list item, is
163+
not found, and nothing reports the miss.
164+
165+
The environment carries the tag for a commit that does not exist yet:
166+
167+
```
168+
NO_UNIT_GUIDE='internal to the LLM flow' git commit ...
169+
```
170+
171+
Reach for that form when the pre-commit hook stops you. A pre-commit hook runs
172+
before git records your message anywhere, and the check declines to read the
173+
previous commit's message in its place, so the environment is the channel left
174+
open. Quote the reason: an unquoted one ends at the first space, and the shell
175+
takes the next word for the command to run, so no commit happens at all. Write
176+
the tag into the message as well, so the reason survives in the history rather
177+
than only in the shell that ran the commit.
178+
179+
A tag carrying no reason waives nothing, in either channel. The reason is the
180+
only record of the decision, so a waiver without one leaves nothing to review.
181+
121182
## Code examples
122183

123184
- One minimal example under "Get started", with enough of the surrounding
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "2.9.2"
2+
".": "2.10.0"
33
}

‎.github/release-please-config.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,5 @@
5757
]
5858
}
5959
},
60-
"last-release-sha": "b65be0155cd68b849809b9a13dc1eda8e43474f6"
60+
"last-release-sha": "84e3265c0b11dc4284a52ca335afedc2f286284e"
6161
}

‎.github/workflows/release-artifact-check.yml‎

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
# limitations under the License.
1414

1515
# 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.
1818
#
1919
# This runs on the release pull request, which is where the version bump and
2020
# the changelog live and where the release oncaller is already looking. It is
@@ -129,3 +129,184 @@ jobs:
129129
fi
130130
gh pr comment "$PR_NUMBER" --body-file release-artifact-check.md --edit-last \
131131
|| 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

Comments
 (0)