Skip to content

Commit 9d54821

Browse files
Kludexdmontagu
andauthored
Fix handling of additionalProperties by gemini (#1506)
Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com>
1 parent 0f3a20d commit 9d54821

File tree

5 files changed

+210
-3
lines changed

5 files changed

+210
-3
lines changed

pydantic_ai_slim/pydantic_ai/models/_json_schema.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ class WalkJsonSchema(ABC):
2020
def __init__(
2121
self, schema: JsonSchema, *, prefer_inlined_defs: bool = False, simplify_nullable_unions: bool = False
2222
):
23-
self.schema = deepcopy(schema)
23+
self.schema = schema
2424
self.prefer_inlined_defs = prefer_inlined_defs
2525
self.simplify_nullable_unions = simplify_nullable_unions
2626

27-
self.defs: dict[str, JsonSchema] = self.schema.pop('$defs', {})
27+
self.defs: dict[str, JsonSchema] = self.schema.get('$defs', {})
2828
self.refs_stack = tuple[str, ...]()
2929
self.recursive_refs = set[str]()
3030

@@ -34,7 +34,11 @@ def transform(self, schema: JsonSchema) -> JsonSchema:
3434
return schema
3535

3636
def walk(self) -> JsonSchema:
37-
handled = self._handle(deepcopy(self.schema))
37+
schema = deepcopy(self.schema)
38+
39+
# First, handle everything but $defs:
40+
schema.pop('$defs', None)
41+
handled = self._handle(schema)
3842

3943
if not self.prefer_inlined_defs and self.defs:
4044
handled['$defs'] = {k: self._handle(v) for k, v in self.defs.items()}

pydantic_ai_slim/pydantic_ai/models/gemini.py

+17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations as _annotations
22

33
import base64
4+
import warnings
45
from collections.abc import AsyncIterator, Sequence
56
from contextlib import asynccontextmanager
67
from dataclasses import dataclass, field, replace
@@ -776,6 +777,22 @@ def __init__(self, schema: JsonSchema):
776777
super().__init__(schema, prefer_inlined_defs=True, simplify_nullable_unions=True)
777778

778779
def transform(self, schema: JsonSchema) -> JsonSchema:
780+
# Note: we need to remove `additionalProperties: False` since it is currently mishandled by Gemini
781+
additional_properties = schema.pop(
782+
'additionalProperties', None
783+
) # don't pop yet so it's included in the warning
784+
if additional_properties: # pragma: no cover
785+
original_schema = {**schema, 'additionalProperties': additional_properties}
786+
warnings.warn(
787+
'`additionalProperties` is not supported by Gemini; it will be removed from the tool JSON schema.'
788+
f' Full schema: {self.schema}\n\n'
789+
f'Source of additionalProperties within the full schema: {original_schema}\n\n'
790+
'If this came from a field with a type like `dict[str, MyType]`, that field will always be empty.\n\n'
791+
"If Google's APIs are updated to support this properly, please create an issue on the PydanticAI GitHub"
792+
' and we will fix this behavior.',
793+
UserWarning,
794+
)
795+
779796
schema.pop('title', None)
780797
schema.pop('default', None)
781798
schema.pop('$schema', None)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- '*/*'
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '296'
12+
content-type:
13+
- application/json
14+
host:
15+
- generativelanguage.googleapis.com
16+
method: POST
17+
parsed_body:
18+
contents:
19+
- parts:
20+
- text: What is the temperature in Tokyo?
21+
role: user
22+
tools:
23+
function_declarations:
24+
- description: null
25+
name: get_temperature
26+
parameters:
27+
properties:
28+
city:
29+
type: string
30+
country:
31+
type: string
32+
required:
33+
- city
34+
- country
35+
type: object
36+
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent
37+
response:
38+
headers:
39+
alt-svc:
40+
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
41+
content-length:
42+
- '748'
43+
content-type:
44+
- application/json; charset=UTF-8
45+
server-timing:
46+
- gfet4t7; dur=523
47+
transfer-encoding:
48+
- chunked
49+
vary:
50+
- Origin
51+
- X-Origin
52+
- Referer
53+
parsed_body:
54+
candidates:
55+
- avgLogprobs: -0.12538558465463143
56+
content:
57+
parts:
58+
- text: |
59+
The available tools lack the ability to access real-time information, including current temperature. Therefore, I cannot answer your question.
60+
role: model
61+
finishReason: STOP
62+
modelVersion: gemini-1.5-flash
63+
usageMetadata:
64+
candidatesTokenCount: 27
65+
candidatesTokensDetails:
66+
- modality: TEXT
67+
tokenCount: 27
68+
promptTokenCount: 14
69+
promptTokensDetails:
70+
- modality: TEXT
71+
tokenCount: 14
72+
totalTokenCount: 41
73+
status:
74+
code: 200
75+
message: OK
76+
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- '*/*'
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '264'
12+
content-type:
13+
- application/json
14+
host:
15+
- generativelanguage.googleapis.com
16+
method: POST
17+
parsed_body:
18+
contents:
19+
- parts:
20+
- text: What is the temperature in Tokyo?
21+
role: user
22+
tools:
23+
function_declarations:
24+
- description: ''
25+
name: get_temperature
26+
parameters:
27+
properties:
28+
location:
29+
type: object
30+
required:
31+
- location
32+
type: object
33+
uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent
34+
response:
35+
headers:
36+
alt-svc:
37+
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
38+
content-length:
39+
- '741'
40+
content-type:
41+
- application/json; charset=UTF-8
42+
server-timing:
43+
- gfet4t7; dur=534
44+
transfer-encoding:
45+
- chunked
46+
vary:
47+
- Origin
48+
- X-Origin
49+
- Referer
50+
parsed_body:
51+
candidates:
52+
- avgLogprobs: -0.15060695580073766
53+
content:
54+
parts:
55+
- text: |
56+
I need a location dictionary to use the `get_temperature` function. I cannot provide the temperature in Tokyo without more information.
57+
role: model
58+
finishReason: STOP
59+
modelVersion: gemini-1.5-flash
60+
usageMetadata:
61+
candidatesTokenCount: 28
62+
candidatesTokensDetails:
63+
- modality: TEXT
64+
tokenCount: 28
65+
promptTokenCount: 12
66+
promptTokensDetails:
67+
- modality: TEXT
68+
tokenCount: 12
69+
totalTokenCount: 40
70+
status:
71+
code: 200
72+
message: OK
73+
version: 1

tests/models/test_gemini.py

+37
Original file line numberDiff line numberDiff line change
@@ -1029,3 +1029,40 @@ async def test_gemini_model_instructions(allow_model_requests: None, gemini_api_
10291029
),
10301030
]
10311031
)
1032+
1033+
1034+
class CurrentLocation(BaseModel, extra='forbid'):
1035+
city: str
1036+
country: str
1037+
1038+
1039+
@pytest.mark.vcr()
1040+
async def test_gemini_additional_properties_is_false(allow_model_requests: None, gemini_api_key: str):
1041+
m = GeminiModel('gemini-1.5-flash', provider=GoogleGLAProvider(api_key=gemini_api_key))
1042+
agent = Agent(m)
1043+
1044+
@agent.tool_plain
1045+
async def get_temperature(location: CurrentLocation) -> float: # pragma: no cover
1046+
return 20.0
1047+
1048+
result = await agent.run('What is the temperature in Tokyo?')
1049+
assert result.output == snapshot(
1050+
'The available tools lack the ability to access real-time information, including current temperature. Therefore, I cannot answer your question.\n'
1051+
)
1052+
1053+
1054+
@pytest.mark.vcr()
1055+
async def test_gemini_additional_properties_is_true(allow_model_requests: None, gemini_api_key: str):
1056+
m = GeminiModel('gemini-1.5-flash', provider=GoogleGLAProvider(api_key=gemini_api_key))
1057+
agent = Agent(m)
1058+
1059+
with pytest.warns(UserWarning, match='.*additionalProperties.*'):
1060+
1061+
@agent.tool_plain
1062+
async def get_temperature(location: dict[str, CurrentLocation]) -> float: # pragma: no cover
1063+
return 20.0
1064+
1065+
result = await agent.run('What is the temperature in Tokyo?')
1066+
assert result.output == snapshot(
1067+
'I need a location dictionary to use the `get_temperature` function. I cannot provide the temperature in Tokyo without more information.\n'
1068+
)

0 commit comments

Comments
 (0)