Skip to content

Commit 4a24370

Browse files
authored
Merge branch 'main' into fix/cli-refactor
2 parents 89c7c88 + f36aaa6 commit 4a24370

File tree

6 files changed

+112
-33
lines changed

6 files changed

+112
-33
lines changed

pydantic_ai_slim/pydantic_ai/_function_schema.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,21 @@ def function_schema( # noqa: C901
154154
if p.kind == Parameter.VAR_POSITIONAL:
155155
annotation = list[annotation]
156156

157-
# FieldInfo.from_annotation expects a type, `annotation` is Any
157+
required = p.default is Parameter.empty
158+
# FieldInfo.from_annotated_attribute expects a type, `annotation` is Any
158159
annotation = cast(type[Any], annotation)
159-
field_info = FieldInfo.from_annotation(annotation)
160+
if required:
161+
field_info = FieldInfo.from_annotation(annotation)
162+
else:
163+
field_info = FieldInfo.from_annotated_attribute(annotation, p.default)
160164
if field_info.description is None:
161165
field_info.description = field_descriptions.get(field_name)
162166

163167
fields[field_name] = td_schema = gen_schema._generate_td_field_schema( # pyright: ignore[reportPrivateUsage]
164168
field_name,
165169
field_info,
166170
decorators,
167-
required=p.default is Parameter.empty,
171+
required=required,
168172
)
169173
# noinspection PyTypeChecker
170174
td_schema.setdefault('metadata', {})['is_model_like'] = is_model_like(annotation)

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,19 @@ def __init__(
312312

313313
def _infer_media_type(self) -> str:
314314
"""Return the media type of the document, based on the url."""
315+
# Common document types are hardcoded here as mime-type support for these
316+
# extensions varies across operating systems.
317+
if self.url.endswith(('.md', '.mdx', '.markdown')):
318+
return 'text/markdown'
319+
elif self.url.endswith('.asciidoc'):
320+
return 'text/x-asciidoc'
321+
elif self.url.endswith('.txt'):
322+
return 'text/plain'
323+
elif self.url.endswith('.pdf'):
324+
return 'application/pdf'
325+
elif self.url.endswith('.rtf'):
326+
return 'application/rtf'
327+
315328
type_, _ = guess_type(self.url)
316329
if type_ is None:
317330
raise ValueError(f'Unknown document file extension: {self.url}')

pydantic_ai_slim/pydantic_ai/profiles/openai.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,6 @@ def openai_model_profile(model_name: str) -> ModelProfile:
4747
_STRICT_INCOMPATIBLE_KEYS = [
4848
'minLength',
4949
'maxLength',
50-
'pattern',
51-
'format',
52-
'minimum',
53-
'maximum',
54-
'multipleOf',
5550
'patternProperties',
5651
'unevaluatedProperties',
5752
'propertyNames',
@@ -61,11 +56,21 @@ def openai_model_profile(model_name: str) -> ModelProfile:
6156
'contains',
6257
'minContains',
6358
'maxContains',
64-
'minItems',
65-
'maxItems',
6659
'uniqueItems',
6760
]
6861

62+
_STRICT_COMPATIBLE_STRING_FORMATS = [
63+
'date-time',
64+
'time',
65+
'date',
66+
'duration',
67+
'email',
68+
'hostname',
69+
'ipv4',
70+
'ipv6',
71+
'uuid',
72+
]
73+
6974
_sentinel = object()
7075

7176

@@ -127,6 +132,9 @@ def transform(self, schema: JsonSchema) -> JsonSchema: # noqa C901
127132
value = schema.get(key, _sentinel)
128133
if value is not _sentinel:
129134
incompatible_values[key] = value
135+
if format := schema.get('format'):
136+
if format not in _STRICT_COMPATIBLE_STRING_FORMATS:
137+
incompatible_values['format'] = format
130138
description = schema.get('description')
131139
if incompatible_values:
132140
if self.strict is True:

tests/models/test_openai.py

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pytest
1313
from dirty_equals import IsListOrTuple
1414
from inline_snapshot import snapshot
15-
from pydantic import BaseModel, Discriminator, Field, Tag
15+
from pydantic import AnyUrl, BaseModel, Discriminator, Field, Tag
1616
from typing_extensions import TypedDict
1717

1818
from pydantic_ai import Agent, ModelHTTPError, ModelRetry, UnexpectedModelBehavior
@@ -1094,6 +1094,14 @@ def tool_with_default(x: int = 1) -> str:
10941094
return f'{x}' # pragma: no cover
10951095

10961096

1097+
def tool_with_datetime(x: datetime) -> str:
1098+
return f'{x}' # pragma: no cover
1099+
1100+
1101+
def tool_with_url(x: AnyUrl) -> str:
1102+
return f'{x}' # pragma: no cover
1103+
1104+
10971105
def tool_with_recursion(x: MyRecursiveDc, y: MyDefaultRecursiveDc):
10981106
return f'{x} {y}' # pragma: no cover
10991107

@@ -1144,12 +1152,50 @@ def tool_with_tuples(x: tuple[int], y: tuple[str] = ('abc',)) -> str:
11441152
snapshot(None),
11451153
),
11461154
(
1147-
strict_compatible_tool,
1155+
tool_with_default,
11481156
None,
11491157
snapshot(
11501158
{
11511159
'additionalProperties': False,
1152-
'properties': {'x': {'type': 'integer'}},
1160+
'properties': {'x': {'default': 1, 'type': 'integer'}},
1161+
'type': 'object',
1162+
}
1163+
),
1164+
snapshot(None),
1165+
),
1166+
(
1167+
tool_with_datetime,
1168+
None,
1169+
snapshot(
1170+
{
1171+
'additionalProperties': False,
1172+
'properties': {'x': {'format': 'date-time', 'type': 'string'}},
1173+
'required': ['x'],
1174+
'type': 'object',
1175+
}
1176+
),
1177+
snapshot(True),
1178+
),
1179+
(
1180+
tool_with_url,
1181+
None,
1182+
snapshot(
1183+
{
1184+
'additionalProperties': False,
1185+
'properties': {'x': {'format': 'uri', 'minLength': 1, 'type': 'string'}},
1186+
'required': ['x'],
1187+
'type': 'object',
1188+
}
1189+
),
1190+
snapshot(None),
1191+
),
1192+
(
1193+
tool_with_url,
1194+
True,
1195+
snapshot(
1196+
{
1197+
'additionalProperties': False,
1198+
'properties': {'x': {'type': 'string', 'description': 'minLength=1, format=uri'}},
11531199
'required': ['x'],
11541200
'type': 'object',
11551201
}
@@ -1413,6 +1459,7 @@ def tool_with_tuples(x: tuple[int], y: tuple[str] = ('abc',)) -> str:
14131459
'properties': {
14141460
'x': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array'},
14151461
'y': {
1462+
'default': ['abc'],
14161463
'maxItems': 1,
14171464
'minItems': 1,
14181465
'prefixItems': [{'type': 'string'}],
@@ -1432,16 +1479,8 @@ def tool_with_tuples(x: tuple[int], y: tuple[str] = ('abc',)) -> str:
14321479
{
14331480
'additionalProperties': False,
14341481
'properties': {
1435-
'x': {
1436-
'prefixItems': [{'type': 'integer'}],
1437-
'type': 'array',
1438-
'description': 'minItems=1, maxItems=1',
1439-
},
1440-
'y': {
1441-
'prefixItems': [{'type': 'string'}],
1442-
'type': 'array',
1443-
'description': 'minItems=1, maxItems=1',
1444-
},
1482+
'x': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array'},
1483+
'y': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'string'}], 'type': 'array'},
14451484
},
14461485
'required': ['x', 'y'],
14471486
'type': 'object',
@@ -1537,9 +1576,10 @@ class MyModel(BaseModel):
15371576
},
15381577
'my_recursive': {'anyOf': [{'$ref': '#'}, {'type': 'null'}]},
15391578
'my_tuple': {
1579+
'maxItems': 1,
1580+
'minItems': 1,
15401581
'prefixItems': [{'type': 'integer'}],
15411582
'type': 'array',
1542-
'description': 'minItems=1, maxItems=1',
15431583
},
15441584
},
15451585
'required': ['my_recursive', 'my_patterns', 'my_tuple', 'my_list', 'my_discriminated_union'],
@@ -1555,11 +1595,7 @@ class MyModel(BaseModel):
15551595
'properties': {},
15561596
'required': [],
15571597
},
1558-
'my_tuple': {
1559-
'prefixItems': [{'type': 'integer'}],
1560-
'type': 'array',
1561-
'description': 'minItems=1, maxItems=1',
1562-
},
1598+
'my_tuple': {'maxItems': 1, 'minItems': 1, 'prefixItems': [{'type': 'integer'}], 'type': 'array'},
15631599
'my_list': {'items': {'type': 'number'}, 'type': 'array'},
15641600
'my_discriminated_union': {'anyOf': [{'$ref': '#/$defs/Apple'}, {'$ref': '#/$defs/Banana'}]},
15651601
},

tests/test_messages.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ def test_youtube_video_url(url: str, is_youtube: bool):
4141
assert video_url.format == 'mp4'
4242

4343

44+
@pytest.mark.parametrize(
45+
'url, expected_data_type',
46+
[
47+
('https://raw.githubusercontent.com/pydantic/pydantic-ai/refs/heads/main/docs/help.md', 'text/markdown'),
48+
('https://raw.githubusercontent.com/pydantic/pydantic-ai/refs/heads/main/docs/help.txt', 'text/plain'),
49+
('https://raw.githubusercontent.com/pydantic/pydantic-ai/refs/heads/main/docs/help.pdf', 'application/pdf'),
50+
('https://raw.githubusercontent.com/pydantic/pydantic-ai/refs/heads/main/docs/help.rtf', 'application/rtf'),
51+
(
52+
'https://raw.githubusercontent.com/pydantic/pydantic-ai/refs/heads/main/docs/help.asciidoc',
53+
'text/x-asciidoc',
54+
),
55+
],
56+
)
57+
def test_document_url_other_types(url: str, expected_data_type: str) -> None:
58+
document_url = DocumentUrl(url=url)
59+
assert document_url.media_type == expected_data_type
60+
61+
4462
def test_document_url():
4563
document_url = DocumentUrl(url='https://example.com/document.pdf')
4664
assert document_url.media_type == 'application/pdf'

tests/test_tools.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ def my_tool_plain(*, a: int = 1, b: int) -> int:
932932
'outer_typed_dict_key': None,
933933
'parameters_json_schema': {
934934
'additionalProperties': False,
935-
'properties': {'a': {'type': 'integer'}, 'b': {'type': 'integer'}},
935+
'properties': {'a': {'type': 'integer'}, 'b': {'default': 1, 'type': 'integer'}},
936936
'required': ['a'],
937937
'type': 'object',
938938
},
@@ -945,7 +945,7 @@ def my_tool_plain(*, a: int = 1, b: int) -> int:
945945
'outer_typed_dict_key': None,
946946
'parameters_json_schema': {
947947
'additionalProperties': False,
948-
'properties': {'a': {'type': 'integer'}, 'b': {'type': 'integer'}},
948+
'properties': {'a': {'default': 1, 'type': 'integer'}, 'b': {'type': 'integer'}},
949949
'required': ['b'],
950950
'type': 'object',
951951
},
@@ -1031,7 +1031,7 @@ def my_tool(x: Annotated[Union[str, None], WithJsonSchema({'type': 'string'})] =
10311031
'name': 'my_tool_1',
10321032
'outer_typed_dict_key': None,
10331033
'parameters_json_schema': {
1034-
'properties': {'x': {'type': 'string'}},
1034+
'properties': {'x': {'default': None, 'type': 'string'}},
10351035
'type': 'object',
10361036
},
10371037
'strict': None,
@@ -1042,7 +1042,7 @@ def my_tool(x: Annotated[Union[str, None], WithJsonSchema({'type': 'string'})] =
10421042
'name': 'my_tool_2',
10431043
'outer_typed_dict_key': None,
10441044
'parameters_json_schema': {
1045-
'properties': {'x': {'type': 'string', 'title': 'X title'}},
1045+
'properties': {'x': {'default': None, 'type': 'string', 'title': 'X title'}},
10461046
'type': 'object',
10471047
},
10481048
'strict': None,

0 commit comments

Comments
 (0)