Skip to content

Commit 61d9408

Browse files
authored
Merge branch 'main' into fix-dep-session.send
2 parents 276a35f + 4124f54 commit 61d9408

41 files changed

Lines changed: 2730 additions & 57 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# MCP Service Account Agent Sample
2+
3+
This agent demonstrates how to connect to a remote MCP server using a gcloud service account for authentication. It uses Streamable HTTP for communication.
4+
5+
## Setup
6+
7+
Before running the agent, you need to configure the MCP server URL and your service account credentials in `agent.py`.
8+
9+
1. **Configure MCP Server URL:**
10+
Update the `MCP_SERVER_URL` variable with the URL of your MCP server instance.
11+
12+
```python
13+
# agent.py
14+
# TODO: Update this to the production MCP server url and scopes.
15+
MCP_SERVER_URL = "https://test.sandbox.googleapis.com/mcp"
16+
```
17+
18+
2. **Set up Service Account Credentials:**
19+
- Obtain the JSON key file for your gcloud service account.
20+
- In `agent.py`, find the `ServiceAccountCredential` object and populate its parameters (e.g., `project_id`, `private_key`, `client_email`, etc.) with the corresponding values from your JSON key file.
21+
22+
```python
23+
# agent.py
24+
# TODO: Update this to the user's service account credentials.
25+
auth_credential=AuthCredential(
26+
auth_type=AuthCredentialTypes.SERVICE_ACCOUNT,
27+
service_account=ServiceAccount(
28+
service_account_credential=ServiceAccountCredential(
29+
type_="service_account",
30+
project_id="example",
31+
private_key_id="123",
32+
private_key="123",
33+
client_email="test@example.iam.gserviceaccount.com",
34+
client_id="123",
35+
auth_uri="https://accounts.google.com/o/oauth2/auth",
36+
token_uri="https://oauth2.googleapis.com/token",
37+
auth_provider_x509_cert_url=(
38+
"https://www.googleapis.com/oauth2/v1/certs"
39+
),
40+
client_x509_cert_url="https://www.googleapis.com/robot/v1/metadata/x509/example.iam.gserviceaccount.com",
41+
universe_domain="googleapis.com",
42+
),
43+
scopes=SCOPES.keys(),
44+
),
45+
),
46+
```
47+
48+
## Running the Agent
49+
50+
Once configured, you can run the agent.
51+
52+
For example:
53+
```bash
54+
adk web
55+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
from fastapi.openapi.models import OAuth2
17+
from fastapi.openapi.models import OAuthFlowClientCredentials
18+
from fastapi.openapi.models import OAuthFlows
19+
from google.adk.agents.llm_agent import LlmAgent
20+
from google.adk.auth.auth_credential import AuthCredential
21+
from google.adk.auth.auth_credential import AuthCredentialTypes
22+
from google.adk.auth.auth_credential import ServiceAccount
23+
from google.adk.auth.auth_credential import ServiceAccountCredential
24+
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPServerParams
25+
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset
26+
27+
# TODO: Update this to the production MCP server url and scopes.
28+
MCP_SERVER_URL = "https://test.sandbox.googleapis.com/mcp"
29+
SCOPES = {"https://www.googleapis.com/auth/cloud-platform": ""}
30+
31+
root_agent = LlmAgent(
32+
model="gemini-2.0-flash",
33+
name="enterprise_assistant",
34+
instruction="""
35+
Help the user with the tools available to you.
36+
""",
37+
tools=[
38+
MCPToolset(
39+
connection_params=StreamableHTTPServerParams(
40+
url=MCP_SERVER_URL,
41+
),
42+
auth_scheme=OAuth2(
43+
flows=OAuthFlows(
44+
clientCredentials=OAuthFlowClientCredentials(
45+
tokenUrl="https://oauth2.googleapis.com/token",
46+
scopes=SCOPES,
47+
)
48+
)
49+
),
50+
# TODO: Update this to the user's service account credentials.
51+
auth_credential=AuthCredential(
52+
auth_type=AuthCredentialTypes.SERVICE_ACCOUNT,
53+
service_account=ServiceAccount(
54+
service_account_credential=ServiceAccountCredential(
55+
type_="service_account",
56+
project_id="example",
57+
private_key_id="123",
58+
private_key="123",
59+
client_email="test@example.iam.gserviceaccount.com",
60+
client_id="123",
61+
auth_uri="https://accounts.google.com/o/oauth2/auth",
62+
token_uri="https://oauth2.googleapis.com/token",
63+
auth_provider_x509_cert_url=(
64+
"https://www.googleapis.com/oauth2/v1/certs"
65+
),
66+
client_x509_cert_url="https://www.googleapis.com/robot/v1/metadata/x509/example.iam.gserviceaccount.com",
67+
universe_domain="googleapis.com",
68+
),
69+
scopes=SCOPES.keys(),
70+
),
71+
),
72+
)
73+
],
74+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Loading and Upgrading Old Session Databases
2+
3+
This example demonstrates how to upgrade a session database created with an older version of ADK to be compatible with the current version.
4+
5+
## Sample Database
6+
7+
This sample includes `dnd_sessions.db`, a database created with ADK v1.15.0. The following steps show how to run into a schema error and then resolve it using the migration script.
8+
9+
## 1. Reproduce the Error
10+
11+
First, copy the old database to `sessions.db`, which is the file the sample application expects.
12+
13+
```bash
14+
cp dnd_sessions.db sessions.db
15+
python main.py
16+
```
17+
18+
Running the application against the old database will fail with a schema mismatch error, as the `events` table is missing a column required by newer ADK versions:
19+
20+
```
21+
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: events.usage_metadata
22+
```
23+
24+
## 2. Upgrade the Database Schema
25+
26+
ADK provides a migration script to update the database schema. Run the following command to download and execute it.
27+
28+
```bash
29+
# Clean up the previous run before executing the migration
30+
cp dnd_sessions.db sessions.db
31+
32+
# Download and run the migration script
33+
curl -fsSL https://raw.githubusercontent.com/google/adk-python/main/scripts/db_migration.sh | sh -s -- "sqlite:///%(here)s/sessions.db" "google.adk.sessions.database_session_service"
34+
```
35+
36+
This script uses `alembic` to compare the existing schema against the current model definition and automatically generates and applies the necessary migrations.
37+
38+
**Note on generated files:**
39+
* The script will create an `alembic.ini` file and an `alembic/` directory. You must delete these before re-running the script.
40+
* The `sample-output` directory in this example contains a reference of the generated files for your inspection.
41+
* The `%(here)s` variable in the database URL is an `alembic` placeholder that refers to the current directory.
42+
43+
## 3. Run the Agent Successfully
44+
45+
With the database schema updated, the application can now load the session correctly.
46+
47+
```bash
48+
python main.py
49+
```
50+
51+
You should see output indicating that the old session was successfully loaded.
52+
53+
## Limitations
54+
55+
The migration script is designed to add new columns that have been introduced in newer ADK versions. It does not handle more complex schema changes, such as modifying a column's data type (e.g., from `int` to `string`) or altering the internal structure of stored data.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
from . import agent
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import random
17+
18+
from google.adk.agents.llm_agent import Agent
19+
20+
21+
def roll_die(sides: int) -> int:
22+
"""Roll a die and return the rolled result.
23+
24+
Args:
25+
sides: The integer number of sides the die has.
26+
27+
Returns:
28+
An integer of the result of rolling the die.
29+
"""
30+
return random.randint(1, sides)
31+
32+
33+
async def check_prime(nums: list[int]) -> str:
34+
"""Check if a given list of numbers are prime.
35+
36+
Args:
37+
nums: The list of numbers to check.
38+
39+
Returns:
40+
A str indicating which number is prime.
41+
"""
42+
primes = set()
43+
for number in nums:
44+
number = int(number)
45+
if number <= 1:
46+
continue
47+
is_prime = True
48+
for i in range(2, int(number**0.5) + 1):
49+
if number % i == 0:
50+
is_prime = False
51+
break
52+
if is_prime:
53+
primes.add(number)
54+
return (
55+
"No prime numbers found."
56+
if not primes
57+
else f"{', '.join(str(num) for num in primes)} are prime numbers."
58+
)
59+
60+
61+
root_agent = Agent(
62+
model="gemini-2.0-flash",
63+
name="migrate_session_db_agent",
64+
description=(
65+
"hello world agent that can roll a dice of 8 sides and check prime"
66+
" numbers."
67+
),
68+
instruction="""
69+
You roll dice and answer questions about the outcome of the dice rolls.
70+
You can roll dice of different sizes.
71+
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
72+
It is ok to discuss previous dice roles, and comment on the dice rolls.
73+
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
74+
You should never roll a die on your own.
75+
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
76+
You should not check prime numbers before calling the tool.
77+
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
78+
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
79+
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
80+
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
81+
3. When you respond, you must include the roll_die result from step 1.
82+
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
83+
You should not rely on the previous history on prime results.
84+
""",
85+
tools=[
86+
roll_die,
87+
check_prime,
88+
],
89+
)
48 KB
Binary file not shown.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
import asyncio
17+
import time
18+
19+
import agent
20+
from dotenv import load_dotenv
21+
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
22+
from google.adk.cli.utils import logs
23+
from google.adk.runners import Runner
24+
from google.adk.sessions.database_session_service import DatabaseSessionService
25+
from google.adk.sessions.session import Session
26+
from google.genai import types
27+
28+
load_dotenv(override=True)
29+
logs.log_to_tmp_folder()
30+
31+
32+
async def main():
33+
app_name = 'migrate_session_db_app'
34+
user_id_1 = 'user1'
35+
session_service = DatabaseSessionService('sqlite:///./sessions.db')
36+
artifact_service = InMemoryArtifactService()
37+
runner = Runner(
38+
app_name=app_name,
39+
agent=agent.root_agent,
40+
artifact_service=artifact_service,
41+
session_service=session_service,
42+
)
43+
session_11 = await session_service.get_session(
44+
app_name=app_name,
45+
user_id=user_id_1,
46+
session_id='aee03f34-32ef-432b-b1bb-e66a3a79dd5b',
47+
)
48+
print('Session 11 loaded:', session_11.id)
49+
50+
async def run_prompt(session: Session, new_message: str):
51+
content = types.Content(
52+
role='user', parts=[types.Part.from_text(text=new_message)]
53+
)
54+
print('** User says:', content.model_dump(exclude_none=True))
55+
async for event in runner.run_async(
56+
user_id=user_id_1,
57+
session_id=session.id,
58+
new_message=content,
59+
):
60+
if event.content.parts and event.content.parts[0].text:
61+
print(f'** {event.author}: {event.content.parts[0].text}')
62+
63+
start_time = time.time()
64+
print('Start time:', start_time)
65+
print('------------------------------------')
66+
await run_prompt(session_11, 'Hi, introduce yourself.')
67+
await run_prompt(
68+
session_11, 'Roll a die with 100 sides and check if it is prime'
69+
)
70+
await run_prompt(session_11, 'Roll it again.')
71+
await run_prompt(session_11, 'What numbers did I got?')
72+
end_time = time.time()
73+
print('------------------------------------')
74+
print('End time:', end_time)
75+
print('Total time:', end_time - start_time)
76+
77+
78+
if __name__ == '__main__':
79+
asyncio.run(main())

0 commit comments

Comments
 (0)