Skip to content

Commit 29f18f4

Browse files
seanzhougooglecopybara-github
authored andcommitted
chore: Add a sample agent to test pydantic models as function tool argument
PiperOrigin-RevId: 814293450
1 parent 571c802 commit 29f18f4

File tree

4 files changed

+435
-0
lines changed

4 files changed

+435
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Pydantic Argument Sample Agent
2+
3+
This sample demonstrates the automatic Pydantic model conversion feature in ADK FunctionTool.
4+
5+
## What This Demonstrates
6+
7+
This sample shows two key features of the Pydantic argument conversion:
8+
9+
### 1. Optional Type Handling
10+
11+
The `create_full_user_account` function demonstrates `Optional[PydanticModel]` conversion:
12+
13+
Before the fix, Optional parameters required manual conversion:
14+
15+
```python
16+
def create_full_user_account(
17+
profile: UserProfile,
18+
preferences: Optional[UserPreferences] = None
19+
) -> dict:
20+
# Manual conversion needed:
21+
if not isinstance(profile, UserProfile):
22+
profile = UserProfile.model_validate(profile)
23+
24+
if preferences is not None and not isinstance(preferences, UserPreferences):
25+
preferences = UserPreferences.model_validate(preferences)
26+
27+
# Your function logic here...
28+
```
29+
30+
**After the fix**, Union/Optional Pydantic models are handled automatically:
31+
32+
```python
33+
def create_full_user_account(
34+
profile: UserProfile,
35+
preferences: Optional[UserPreferences] = None
36+
) -> dict:
37+
# Both profile and preferences are guaranteed to be proper instances!
38+
# profile: UserProfile instance (converted from JSON)
39+
# preferences: UserPreferences instance OR None (converted from JSON or kept as None)
40+
return {"profile": profile.name, "theme": preferences.theme if preferences else "default"}
41+
```
42+
43+
### 2. Union Type Handling
44+
45+
The `create_entity_profile` function demonstrates `Union[PydanticModel1, PydanticModel2]` conversion:
46+
47+
**Before the fix**, Union types required complex manual type checking:
48+
49+
```python
50+
def create_entity_profile(entity: Union[UserProfile, CompanyProfile]) -> dict:
51+
# Manual conversion needed:
52+
if isinstance(entity, dict):
53+
# Try to determine which model to use and convert manually
54+
if 'company_name' in entity:
55+
entity = CompanyProfile.model_validate(entity)
56+
elif 'name' in entity:
57+
entity = UserProfile.model_validate(entity)
58+
else:
59+
raise ValueError("Cannot determine entity type")
60+
# Your function logic here...
61+
```
62+
63+
**After the fix**, Union Pydantic models are handled automatically:
64+
65+
```python
66+
def create_entity_profile(entity: Union[UserProfile, CompanyProfile]) -> dict:
67+
# entity is guaranteed to be either UserProfile or CompanyProfile instance!
68+
# The LLM sends appropriate JSON structure, and it gets converted
69+
# to the correct Pydantic model based on JSON schema matching
70+
if isinstance(entity, UserProfile):
71+
return {"type": "user", "name": entity.name}
72+
else: # CompanyProfile
73+
return {"type": "company", "name": entity.company_name}
74+
```
75+
76+
## How to Run
77+
78+
1. **Set up API credentials** (choose one):
79+
80+
**Option A: Google AI API**
81+
```bash
82+
export GOOGLE_GENAI_API_KEY="your-api-key"
83+
```
84+
85+
**Option B: Vertex AI (requires Google Cloud project)**
86+
```bash
87+
export GOOGLE_CLOUD_PROJECT="your-project-id"
88+
export GOOGLE_CLOUD_LOCATION="us-central1"
89+
```
90+
91+
2. **Run the sample**:
92+
```bash
93+
cd contributing/samples
94+
python -m pydantic_argument.main
95+
```
96+
97+
## Expected Output
98+
99+
The agent will be prompted to create user profiles and accounts, demonstrating automatic Pydantic model conversion.
100+
101+
### Test Scenarios:
102+
103+
1. **Full Account with Preferences (Optional Type)**:
104+
- **Input**: "Create an account for Alice, 25 years old, with dark theme and Spanish language preferences"
105+
- **Tool Called**: `create_full_user_account(profile=UserProfile(...), preferences=UserPreferences(...))`
106+
- **Conversion**: Two JSON dicts → `UserProfile` + `UserPreferences` instances
107+
108+
2. **Account with Different Preferences (Optional Type)**:
109+
- **Input**: "Create a user account for Bob, age 30, with light theme, French language, and notifications disabled"
110+
- **Tool Called**: `create_full_user_account(profile=UserProfile(...), preferences=UserPreferences(...))`
111+
- **Conversion**: Two JSON dicts → `UserProfile` + `UserPreferences` instances
112+
113+
3. **Account with Default Preferences (Optional Type)**:
114+
- **Input**: "Make an account for Charlie, 28 years old, but use default preferences"
115+
- **Tool Called**: `create_full_user_account(profile=UserProfile(...), preferences=None)`
116+
- **Conversion**: JSON dict → `UserProfile`, None → None (Optional handling)
117+
118+
4. **Company Profile Creation (Union Type)**:
119+
- **Input**: "Create a profile for Tech Corp company, software industry, with 150 employees"
120+
- **Tool Called**: `create_entity_profile(entity=CompanyProfile(...))`
121+
- **Conversion**: JSON dict → `CompanyProfile` instance (Union type resolution)
122+
123+
5. **User Profile Creation (Union Type)**:
124+
- **Input**: "Create an entity profile for Diana, 32 years old"
125+
- **Tool Called**: `create_entity_profile(entity=UserProfile(...))`
126+
- **Conversion**: JSON dict → `UserProfile` instance (Union type resolution)
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: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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+
"""Simple agent demonstrating Pydantic model arguments in tools."""
16+
17+
from typing import Optional
18+
from typing import Union
19+
20+
from google.adk.agents.llm_agent import Agent
21+
from google.adk.tools.function_tool import FunctionTool
22+
import pydantic
23+
24+
25+
class UserProfile(pydantic.BaseModel):
26+
"""A user's profile information."""
27+
28+
name: str
29+
age: int
30+
email: Optional[str] = None
31+
32+
33+
class UserPreferences(pydantic.BaseModel):
34+
"""A user's preferences."""
35+
36+
theme: str = "light"
37+
language: str = "English"
38+
notifications_enabled: bool = True
39+
40+
41+
class CompanyProfile(pydantic.BaseModel):
42+
"""A company's profile information."""
43+
44+
company_name: str
45+
industry: str
46+
employee_count: int
47+
website: Optional[str] = None
48+
49+
50+
def create_full_user_account(
51+
profile: UserProfile, preferences: Optional[UserPreferences] = None
52+
) -> dict:
53+
"""Create a complete user account with profile and optional preferences.
54+
55+
This function demonstrates Union/Optional Pydantic model handling.
56+
The preferences parameter is Optional[UserPreferences], which is
57+
internally Union[UserPreferences, None].
58+
59+
Before the fix, we would need:
60+
if preferences is not None and not isinstance(preferences, UserPreferences):
61+
preferences = UserPreferences.model_validate(preferences)
62+
63+
Now the FunctionTool automatically handles this conversion!
64+
65+
Args:
66+
profile: The user's profile information (required)
67+
preferences: Optional user preferences (Union[UserPreferences, None])
68+
69+
Returns:
70+
A dictionary containing the complete user account.
71+
"""
72+
# Use default preferences if not provided
73+
if preferences is None:
74+
preferences = UserPreferences()
75+
76+
# Both profile and preferences are guaranteed to be proper Pydantic instances!
77+
return {
78+
"status": "account_created",
79+
"message": f"Full account created for {profile.name}!",
80+
"profile": {
81+
"name": profile.name,
82+
"age": profile.age,
83+
"email": profile.email or "Not provided",
84+
"profile_type": type(profile).__name__,
85+
},
86+
"preferences": {
87+
"theme": preferences.theme,
88+
"language": preferences.language,
89+
"notifications_enabled": preferences.notifications_enabled,
90+
"preferences_type": type(preferences).__name__,
91+
},
92+
"conversion_demo": {
93+
"profile_converted": "JSON dict → UserProfile instance",
94+
"preferences_converted": (
95+
"JSON dict → UserPreferences instance"
96+
if preferences
97+
else "None → default UserPreferences"
98+
),
99+
},
100+
}
101+
102+
103+
def create_entity_profile(entity: Union[UserProfile, CompanyProfile]) -> dict:
104+
"""Create a profile for either a user or a company.
105+
106+
This function demonstrates Union type handling with multiple Pydantic models.
107+
The entity parameter accepts Union[UserProfile, CompanyProfile].
108+
109+
Before the fix, we would need complex type checking:
110+
if isinstance(entity, dict):
111+
# Try to determine which model to use and convert manually
112+
if 'company_name' in entity:
113+
entity = CompanyProfile.model_validate(entity)
114+
elif 'name' in entity:
115+
entity = UserProfile.model_validate(entity)
116+
else:
117+
raise ValueError("Cannot determine entity type")
118+
119+
Now the FunctionTool automatically handles Union type conversion!
120+
The LLM will send the appropriate JSON structure, and it gets converted
121+
to the correct Pydantic model based on the JSON schema matching.
122+
123+
Args:
124+
entity: Either a UserProfile or CompanyProfile (Union type)
125+
126+
Returns:
127+
A dictionary containing the entity profile information.
128+
"""
129+
if isinstance(entity, UserProfile):
130+
return {
131+
"status": "user_profile_created",
132+
"entity_type": "user",
133+
"message": f"User profile created for {entity.name}!",
134+
"profile": {
135+
"name": entity.name,
136+
"age": entity.age,
137+
"email": entity.email or "Not provided",
138+
"model_type": type(entity).__name__,
139+
},
140+
}
141+
elif isinstance(entity, CompanyProfile):
142+
return {
143+
"status": "company_profile_created",
144+
"entity_type": "company",
145+
"message": f"Company profile created for {entity.company_name}!",
146+
"profile": {
147+
"company_name": entity.company_name,
148+
"industry": entity.industry,
149+
"employee_count": entity.employee_count,
150+
"website": entity.website or "Not provided",
151+
"model_type": type(entity).__name__,
152+
},
153+
}
154+
else:
155+
return {
156+
"status": "error",
157+
"message": f"Unexpected entity type: {type(entity)}",
158+
}
159+
160+
161+
# Create the agent with all Pydantic tools
162+
root_agent = Agent(
163+
model="gemini-2.5-pro",
164+
name="profile_agent",
165+
description=(
166+
"Helpful assistant that helps creating accounts and profiles for users"
167+
" and companies"
168+
),
169+
instruction="""
170+
You are a helpful assistant that can create accounts and profiles for users and companies.
171+
172+
When someone asks you to create a user account, use `create_full_user_account`.
173+
When someone asks you to create a profile and it's unclear whether they mean a user or company, use `create_entity_profile`.
174+
When someone specifically mentions a company, use `create_entity_profile`.
175+
176+
Use the tools with the structured data provided by the user.
177+
""",
178+
tools=[
179+
FunctionTool(create_full_user_account),
180+
FunctionTool(create_entity_profile),
181+
],
182+
)

0 commit comments

Comments
 (0)