Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# BigQuery Agent Analytics Plugin

`BigQueryAgentAnalyticsPlugin` writes agent events to a BigQuery table through
the Write API, so runs can be analysed with SQL. It is registered on the
runner, so it records every agent, model and tool call without touching agent
code.

**Constructor arguments:**

- `project_id`, `dataset_id`: where the events go. The dataset must already
exist; the plugin creates the table.
- `table_id`: table name, `agent_events` by default.
- `config`: a `BigQueryLoggerConfig` for batching, retries, event allow and
deny lists, content truncation and schema upgrades.
- `location`: BigQuery location, `US` by default.
- `credentials`: Application Default Credentials when unset.

## Before you run it

1. Install the extra: `pip install "google-adk[bigquery-analytics]"`.
1. Create the dataset, for example
`bq --location=US mk --dataset PROJECT:agent_analytics`.
1. Grant the account running the agent BigQuery Data Editor
(`roles/bigquery.dataEditor`) on the dataset and BigQuery Job User
(`roles/bigquery.jobUser`) on the project.

## Sample

The agent looks up delivery status for two orders, so the run produces model
calls and tool calls worth reading back.

```bash
export GOOGLE_CLOUD_PROJECT=your-project
export BIGQUERY_ANALYTICS_DATASET=agent_analytics
python contributing/samples/plugins/plugin_bigquery_agent_analytics/main.py
```

Output:

```
user: What is the status of order A1?
agent: Order A1 is currently in transit.

user: And order B2?
agent: Order B2 is currently in transit.

Events written to your-project.agent_analytics.agent_events
```

Reading the events back:

```sql
SELECT event_type, COUNT(*) AS events
FROM `your-project.agent_analytics.agent_events`
GROUP BY event_type ORDER BY events DESC
```

```
LLM_REQUEST 4
LLM_RESPONSE 4
USER_MESSAGE_RECEIVED 2
INVOCATION_STARTING 2
AGENT_STARTING 2
TOOL_STARTING 2
TOOL_COMPLETED 2
AGENT_RESPONSE 2
AGENT_COMPLETED 2
INVOCATION_COMPLETED 2
```

The table is partitioned by day on `timestamp`, so cost stays predictable as
runs accumulate.

## Shutting down cleanly

The plugin writes in the background on a shared transport, so a short script
should release it on the way out:

```python
await plugin.close(close_background_transport=True)
```

A long-running server leaves it open instead, so later turns reuse the
connection. Either way the shared transport is drained at interpreter exit.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from .main import root_agent
104 changes: 104 additions & 0 deletions contributing/samples/plugins/plugin_bigquery_agent_analytics/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Logs agent events to BigQuery with the agent analytics plugin.

Needs the bigquery-analytics extra: pip install "google-adk[bigquery-analytics]"

Set BIGQUERY_ANALYTICS_DATASET to a dataset that already exists. The plugin
creates the table inside it.
"""

import asyncio
import os
import sys

from google.adk import Agent
from google.adk.plugins.bigquery_agent_analytics_plugin import BigQueryAgentAnalyticsPlugin
from google.adk.runners import InMemoryRunner
from google.adk.tools.tool_context import ToolContext
from google.genai import types

APP_NAME = 'plugin_bigquery_agent_analytics'
TABLE_ID = 'agent_events'

PROMPTS = [
'What is the status of order A1?',
'And order B2?',
]


async def lookup_order(tool_context: ToolContext, order_id: str) -> dict:
"""Returns a canned delivery status for an order."""
return {'order_id': order_id, 'status': 'in transit'}


root_agent = Agent(
model='gemini-2.5-flash',
name='order_agent',
description='Looks up delivery status for orders.',
instruction='Use the lookup_order tool to answer questions about an order.',
tools=[lookup_order],
)


async def main():
project_id = os.environ.get('GOOGLE_CLOUD_PROJECT')
dataset_id = os.environ.get('BIGQUERY_ANALYTICS_DATASET')
if not project_id or not dataset_id:
sys.exit(
'Set GOOGLE_CLOUD_PROJECT and BIGQUERY_ANALYTICS_DATASET to a project'
' and an existing BigQuery dataset.'
)

plugin = BigQueryAgentAnalyticsPlugin(
project_id=project_id,
dataset_id=dataset_id,
table_id=TABLE_ID,
)
runner = InMemoryRunner(agent=root_agent, app_name=APP_NAME, plugins=[plugin])
session = await runner.session_service.create_session(
user_id='user', app_name=APP_NAME
)

try:
for prompt in PROMPTS:
print(f'\nuser: {prompt}')
async for event in runner.run_async(
user_id='user',
session_id=session.id,
new_message=types.Content(
role='user', parts=[types.Part.from_text(text=prompt)]
),
):
if event.content and event.content.parts and event.author != 'user':
for part in event.content.parts:
if part.text:
print(f'agent: {part.text.strip()}')
finally:
# A script should release the shared gRPC transport on the way out; a
# long-running server leaves it open so later turns reuse the connection.
await plugin.close(close_background_transport=True)

print(f'\nEvents written to {project_id}.{dataset_id}.{TABLE_ID}')
print('Inspect them with:')
print(
' bq query --nouse_legacy_sql "SELECT event_type, COUNT(*) AS events'
f' FROM \\`{project_id}.{dataset_id}.{TABLE_ID}\\` GROUP BY event_type"'
)


if __name__ == '__main__':
asyncio.run(main())
74 changes: 74 additions & 0 deletions contributing/samples/plugins/plugin_context_filter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Context Filter Plugin

`ContextFilterPlugin` trims the conversation that each model request carries,
so a long session does not keep growing the prompt. Only the request is
trimmed; the session keeps every event.

**Options:**

- `num_invocations_to_keep`: how many recent invocations stay in the request.
An invocation starts with one or more consecutive user messages and covers
every model turn and tool call until the next user message.
- `custom_filter`: a function that receives the contents and returns the ones
to keep, for rules of your own.
- `remove_amount`: how many invocations to drop at once when the limit is
passed.

The plugin keeps function calls paired with their responses, so trimming never
leaves a tool response whose matching call was dropped.

## Sample

The agent looks up delivery status for orders, and the sample asks about three
of them before asking what has been discussed so far. It registers two plugins:

```python
plugins=[
ContextFilterPlugin(num_invocations_to_keep=2),
ContentCounterPlugin(),
]
```

`ContentCounterPlugin` is a few lines defined in the sample. It prints how many
contents each request carries. Plugin callbacks run in registration order, so
it sees the request after the filter has trimmed it.

Run it with:

```bash
python contributing/samples/plugins/plugin_context_filter/main.py
```

Output:

```
user: Look up the delivery status for order A1.
[request 1] contents sent to the model: 1
[request 2] contents sent to the model: 3
agent: Order A1 is in transit.

user: Now look up order B2.
[request 3] contents sent to the model: 5
[request 4] contents sent to the model: 7
agent: Order B2 is in transit.

user: Now look up order C3.
[request 5] contents sent to the model: 5
[request 6] contents sent to the model: 7
agent: Order C3 is in transit.

user: Which orders have I asked about so far?
[request 7] contents sent to the model: 5
agent: You have only asked about order C3.

events kept in the session: 14
```

Two things to read from that output:

- The request stops growing. Without the plugin the count keeps climbing with
every turn; here it settles at 5 to 7 contents.
- The last answer is wrong about history, and that is the trade-off. The model
can only answer from what it still sees, so keep enough invocations for the
questions your agent has to answer, or use `custom_filter` to keep the parts
that matter.
16 changes: 16 additions & 0 deletions contributing/samples/plugins/plugin_context_filter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from .main import root_agent
Loading
Loading