Skip to content

Commit 2b44e59

Browse files
Merge branch 'main' into fix/litellm-strip-thought-signature-from-tool-call-id
2 parents 96aac45 + 07455ee commit 2b44e59

4 files changed

Lines changed: 154 additions & 9 deletions

File tree

scripts/check_new_py_files.sh

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
# limitations under the License.
1515

1616

17+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
19+
ADK_REAL_ROOT=$(dirname "$(realpath "$REPO_ROOT/src/google/adk/__init__.py")")
20+
EXCLUDE_TESTS="$ADK_REAL_ROOT/tests"
21+
EXCLUDE_WORKSPACE="$ADK_REAL_ROOT/open_source_workspace"
22+
EXCLUDE_CONTRIBUTING="$ADK_REAL_ROOT/contributing"
23+
1724
exit_code=0
1825

1926
get_added_files() {
@@ -33,11 +40,17 @@ get_added_files() {
3340
while read -r file; do
3441
# Check if file is not empty (happens if no new files)
3542
if [[ -n "$file" ]]; then
36-
# Match only files in the package source (src/google/adk/) to avoid false
37-
# positives in environments (e.g., monorepos) where the entire repository
38-
# root is nested under a 'google/adk/' directory structure.
39-
if [[ "$file" == */src/google/adk/*.py ]] || [[ "$file" == src/google/adk/*.py ]]; then
40-
filename=$(basename "$file")
43+
# Match only files in the package source (resolving symlinks to support both
44+
# open-source and internal layouts) and exclude tests/workspace to avoid
45+
# false positives.
46+
abs_file=$(realpath "$file" 2>/dev/null || echo "")
47+
if [[ -n "$abs_file" ]] && \
48+
[[ "$abs_file" == "$ADK_REAL_ROOT"/* ]] && \
49+
[[ "$abs_file" != "$EXCLUDE_TESTS"/* ]] && \
50+
[[ "$abs_file" != "$EXCLUDE_WORKSPACE"/* ]] && \
51+
[[ "$abs_file" != "$EXCLUDE_CONTRIBUTING"/* ]] && \
52+
[[ "$abs_file" == *.py ]]; then
53+
filename=$(basename "$abs_file")
4154
if [[ ! "$filename" == _* ]]; then
4255
echo "Error: New Python file '$file' must have a '_' prefix."
4356
echo "All new Python files in src/google/adk/ must be private by default."

src/google/adk/labs/antigravity/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import os
16+
17+
os.environ.setdefault("TEMPORARILY_DISABLE_PROTOBUF_VERSION_CHECK", "true")
18+
1519
try:
1620
import google.antigravity # noqa: F401
1721
except ImportError as e:
1822
raise ImportError(
1923
"The 'google-antigravity' package is required to use the ADK"
20-
' Antigravity integration. Install it with: pip install'
24+
" Antigravity integration. Install it with: pip install"
2125
' "google-adk[antigravity]"'
2226
) from e
2327

2428
from ._antigravity_agent import AntigravityAgent
2529

2630
__all__ = [
27-
'AntigravityAgent',
31+
"AntigravityAgent",
2832
]

src/google/adk/plugins/bigquery_agent_analytics_plugin.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,8 +1015,14 @@ class BigQueryLoggerConfig:
10151015
views all stay consistent); views that reference a denied column drop
10161016
the dependent derived columns. NOTE: denying ``attributes`` also
10171017
disables ``attributes.otel`` and ``attributes.custom_metadata``;
1018-
combining it with a non-empty ``custom_metadata_allowlist`` is
1019-
rejected at construction.
1018+
combining it with a non-empty ``custom_metadata_allowlist`` is rejected
1019+
at construction.
1020+
final_response_tool_names: Tool names whose successful completion carries
1021+
the agent's final answer. When a completed tool's name is in this set,
1022+
its call args are logged as an ``AGENT_RESPONSE`` event. For agents that
1023+
emit the final answer via a dedicated tool (e.g.
1024+
``submit_final_response``) rather than a plain-text final event. Empty
1025+
(the default) preserves today's behavior.
10201026
"""
10211027

10221028
enabled: bool = True
@@ -1081,6 +1087,17 @@ class BigQueryLoggerConfig:
10811087
# are protected (see ``_PROJECTABLE_PAYLOAD_COLUMNS``).
10821088
payload_column_denylist: list[str] | None = None
10831089

1090+
# --- final-answer-via-tool capture ---
1091+
# Tool names whose successful completion carries the agent's final
1092+
# response. Some agents deliver their final answer through a dedicated
1093+
# tool (e.g. ``submit_final_response``) instead of a plain-text final
1094+
# event, so the on-event ``AGENT_RESPONSE`` path (which excludes function
1095+
# calls/responses) never fires. When a completed tool's name is in this
1096+
# set, its call args (the final-answer payload) are logged as an
1097+
# ``AGENT_RESPONSE`` event. Empty (the default) preserves today's
1098+
# behavior.
1099+
final_response_tool_names: frozenset[str] = frozenset()
1100+
10841101

10851102
# ==============================================================================
10861103
# HELPER: TRACE MANAGER (Async-Safe with ContextVars)
@@ -5147,6 +5164,25 @@ async def after_tool_callback(
51475164
event_data=event_data,
51485165
)
51495166

5167+
# Some agents deliver their final answer through a dedicated tool
5168+
# (e.g. ``submit_final_response``) rather than a plain-text final event,
5169+
# so the on-event AGENT_RESPONSE path (which excludes function
5170+
# calls/responses) never fires. When such a tool completes, log its call
5171+
# args (the final-answer payload the model supplied) as AGENT_RESPONSE so
5172+
# the visible response text is captured. Opt-in via
5173+
# ``config.final_response_tool_names`` (empty by default).
5174+
if tool.name in self.config.final_response_tool_names:
5175+
args_truncated, args_is_truncated = _recursive_smart_truncate(
5176+
tool_args, self.config.max_content_length
5177+
)
5178+
await self._log_event(
5179+
"AGENT_RESPONSE",
5180+
tool_context,
5181+
raw_content={"response": args_truncated},
5182+
is_truncated=args_is_truncated,
5183+
event_data=EventData(extra_attributes={"source_tool": tool.name}),
5184+
)
5185+
51505186
@_safe_callback
51515187
async def on_tool_error_callback(
51525188
self,

tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,98 @@ async def test_max_content_length_tool_result_no_truncation(
10411041
assert content_dict["tool"] == "MyTool"
10421042
assert content_dict["result"]["res"] == "A" * 100
10431043

1044+
@pytest.mark.asyncio
1045+
async def test_after_tool_callback_logs_agent_response_for_final_tool(
1046+
self,
1047+
mock_write_client,
1048+
tool_context,
1049+
mock_auth_default,
1050+
mock_bq_client,
1051+
mock_to_arrow_schema,
1052+
dummy_arrow_schema,
1053+
mock_asyncio_to_thread,
1054+
):
1055+
"""A configured final-response tool also logs AGENT_RESPONSE from its args."""
1056+
_ = mock_auth_default
1057+
_ = mock_bq_client
1058+
_ = mock_to_arrow_schema
1059+
_ = mock_asyncio_to_thread
1060+
config = bigquery_agent_analytics_plugin.BigQueryLoggerConfig(
1061+
final_response_tool_names=frozenset({"submit_final_response"})
1062+
)
1063+
async with managed_plugin(
1064+
PROJECT_ID, DATASET_ID, table_id=TABLE_ID, config=config
1065+
) as plugin:
1066+
await plugin._ensure_started()
1067+
mock_write_client.append_rows.reset_mock()
1068+
mock_tool = mock.create_autospec(
1069+
base_tool_lib.BaseTool, instance=True, spec_set=True
1070+
)
1071+
type(mock_tool).name = mock.PropertyMock(
1072+
return_value="submit_final_response"
1073+
)
1074+
bigquery_agent_analytics_plugin.TraceManager.push_span(tool_context)
1075+
await plugin.after_tool_callback(
1076+
tool=mock_tool,
1077+
tool_args={"answer": "The table has 241 rows."},
1078+
tool_context=tool_context,
1079+
result={"status": "SUCCESS"},
1080+
)
1081+
await plugin.flush()
1082+
rows = await _get_captured_rows_async(
1083+
mock_write_client, dummy_arrow_schema
1084+
)
1085+
event_types = [r["event_type"] for r in rows]
1086+
assert "TOOL_COMPLETED" in event_types
1087+
assert event_types.count("AGENT_RESPONSE") == 1
1088+
agent_resp = next(r for r in rows if r["event_type"] == "AGENT_RESPONSE")
1089+
content_dict = json.loads(agent_resp["content"])
1090+
assert content_dict["response"] == {"answer": "The table has 241 rows."}
1091+
attributes = json.loads(agent_resp["attributes"])
1092+
assert attributes["source_tool"] == "submit_final_response"
1093+
1094+
@pytest.mark.asyncio
1095+
async def test_after_tool_callback_no_agent_response_by_default(
1096+
self,
1097+
mock_write_client,
1098+
tool_context,
1099+
mock_auth_default,
1100+
mock_bq_client,
1101+
mock_to_arrow_schema,
1102+
dummy_arrow_schema,
1103+
mock_asyncio_to_thread,
1104+
):
1105+
"""With the default empty set, a tool never emits AGENT_RESPONSE."""
1106+
_ = mock_auth_default
1107+
_ = mock_bq_client
1108+
_ = mock_to_arrow_schema
1109+
_ = mock_asyncio_to_thread
1110+
config = bigquery_agent_analytics_plugin.BigQueryLoggerConfig()
1111+
async with managed_plugin(
1112+
PROJECT_ID, DATASET_ID, table_id=TABLE_ID, config=config
1113+
) as plugin:
1114+
await plugin._ensure_started()
1115+
mock_write_client.append_rows.reset_mock()
1116+
mock_tool = mock.create_autospec(
1117+
base_tool_lib.BaseTool, instance=True, spec_set=True
1118+
)
1119+
type(mock_tool).name = mock.PropertyMock(
1120+
return_value="submit_final_response"
1121+
)
1122+
bigquery_agent_analytics_plugin.TraceManager.push_span(tool_context)
1123+
await plugin.after_tool_callback(
1124+
tool=mock_tool,
1125+
tool_args={"answer": "hi"},
1126+
tool_context=tool_context,
1127+
result={"status": "SUCCESS"},
1128+
)
1129+
await plugin.flush()
1130+
rows = await _get_captured_rows_async(
1131+
mock_write_client, dummy_arrow_schema
1132+
)
1133+
event_types = [r["event_type"] for r in rows]
1134+
assert "AGENT_RESPONSE" not in event_types
1135+
10441136
@pytest.mark.asyncio
10451137
async def test_max_content_length_tool_error(
10461138
self,

0 commit comments

Comments
 (0)