Skip to content

Commit 9883cd8

Browse files
committed
Removed Magic Classes logic implemented directly on BinaryContent and DocumentUrl
1 parent 0521b21 commit 9883cd8

File tree

5 files changed

+109
-171
lines changed

5 files changed

+109
-171
lines changed

examples/pydantic_ai_examples/magic_files.py renamed to examples/pydantic_ai_examples/textlike_file_mapping.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
from __future__ import annotations
1111

12-
from pydantic_ai import Agent, MagicBinaryContent, MagicDocumentUrl
12+
from pydantic_ai import Agent
13+
from pydantic_ai.messages import BinaryContent, DocumentUrl
1314

1415
# Load API keys from .env if available
1516
try: # pragma: no cover - example bootstrap
@@ -24,24 +25,21 @@ def run_with_openai() -> None:
2425
agent = Agent('openai:gpt-4o')
2526

2627
# Text file by URL → becomes inline text with a file delimiter on OpenAI
27-
txt_url = MagicDocumentUrl(
28+
txt_url = DocumentUrl(
2829
url='https://raw.githubusercontent.com/pydantic/pydantic/main/README.md',
29-
filename='README.md',
3030
# media_type optional; inferred from extension if omitted
3131
media_type='text/plain',
3232
)
3333

3434
# Binary text (bytes) → becomes inline text with a file delimiter on OpenAI
35-
txt_bytes = MagicBinaryContent(
35+
txt_bytes = BinaryContent(
3636
data=b'Hello from bytes',
3737
media_type='text/plain',
38-
filename='hello.txt',
3938
)
4039

4140
# PDF by URL → remains a file part (base64 + strict MIME) on OpenAI
42-
pdf_url = MagicDocumentUrl(
41+
pdf_url = DocumentUrl(
4342
url='https://arxiv.org/pdf/2403.05530.pdf',
44-
filename='gemini-tech-report.pdf',
4543
media_type='application/pdf',
4644
)
4745

@@ -59,14 +57,12 @@ def run_with_openai() -> None:
5957
def run_with_anthropic() -> None:
6058
agent = Agent('anthropic:claude-3-5-sonnet-latest')
6159

62-
txt_url = MagicDocumentUrl(
60+
txt_url = DocumentUrl(
6361
url='https://raw.githubusercontent.com/pydantic/pydantic/main/README.md',
64-
filename='README.md',
6562
media_type='text/plain',
6663
)
67-
pdf_url = MagicDocumentUrl(
64+
pdf_url = DocumentUrl(
6865
url='https://arxiv.org/pdf/2403.05530.pdf',
69-
filename='gemini-tech-report.pdf',
7066
media_type='application/pdf',
7167
)
7268

pydantic_ai_slim/pydantic_ai/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
BinaryContent,
2828
DocumentUrl,
2929
ImageUrl,
30-
MagicBinaryContent,
31-
MagicDocumentUrl,
3230
VideoUrl,
3331
)
3432
from .output import NativeOutput, PromptedOutput, StructuredDict, TextOutput, ToolOutput
@@ -62,8 +60,6 @@
6260
'VideoUrl',
6361
'DocumentUrl',
6462
'BinaryContent',
65-
'MagicDocumentUrl',
66-
'MagicBinaryContent',
6763
# tools
6864
'Tool',
6965
'ToolDefinition',

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 1 addition & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -529,76 +529,7 @@ def format(self) -> str:
529529
__repr__ = _utils.dataclasses_no_defaults_repr
530530

531531

532-
@dataclass(init=False, repr=False)
533-
class MagicDocumentUrl(DocumentUrl):
534-
"""A provider-agnostic document URL that may be transformed per adapter.
535-
536-
For OpenAI, text/plain documents may be converted to a plain text
537-
`UserContent`.
538-
"""
539-
540-
filename: str | None = None
541-
"""Optional filename hint to use when converting to text."""
542-
543-
is_magic: Literal[True] = True
544-
"""Marker for serialization/filtering to indicate this is a magic part."""
545-
546-
def __init__(
547-
self,
548-
url: str,
549-
*,
550-
force_download: bool = False,
551-
vendor_metadata: dict[str, Any] | None = None,
552-
media_type: str | None = None,
553-
filename: str | None = None,
554-
identifier: str | None = None,
555-
_media_type: str | None = None,
556-
) -> None:
557-
super().__init__(
558-
url=url,
559-
force_download=force_download,
560-
vendor_metadata=vendor_metadata,
561-
media_type=media_type or _media_type,
562-
identifier=identifier,
563-
)
564-
# Keep kind as 'document-url' for downstream OTEL/type expectations
565-
self.filename = filename
566-
567-
568-
@dataclass(init=False, repr=False)
569-
class MagicBinaryContent(BinaryContent):
570-
"""A provider-agnostic binary content that may be transformed per adapter.
571-
572-
For OpenAI, text/plain content may be converted to a plain text
573-
`UserContent`.
574-
"""
575-
576-
filename: str | None = None
577-
"""Optional filename hint to use when converting to text."""
578-
579-
is_magic: Literal[True] = True
580-
"""Marker for serialization/filtering to indicate this is a magic part."""
581-
582-
def __init__(
583-
self,
584-
data: bytes,
585-
*,
586-
media_type: AudioMediaType | ImageMediaType | DocumentMediaType | str,
587-
filename: str | None = None,
588-
identifier: str | None = None,
589-
vendor_metadata: dict[str, Any] | None = None,
590-
) -> None:
591-
super().__init__(
592-
data=data,
593-
media_type=media_type,
594-
identifier=identifier,
595-
vendor_metadata=vendor_metadata,
596-
)
597-
# Keep kind as 'binary' for downstream OTEL/type expectations
598-
self.filename = filename
599-
600-
601-
MultiModalContent = ImageUrl | AudioUrl | DocumentUrl | VideoUrl | BinaryContent | MagicDocumentUrl | MagicBinaryContent
532+
MultiModalContent = ImageUrl | AudioUrl | DocumentUrl | VideoUrl | BinaryContent
602533
UserContent: TypeAlias = str | MultiModalContent
603534

604535

pydantic_ai_slim/pydantic_ai/models/openai.py

Lines changed: 27 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
DocumentUrl,
2727
FinishReason,
2828
ImageUrl,
29-
MagicBinaryContent,
30-
MagicDocumentUrl,
3129
ModelMessage,
3230
ModelRequest,
3331
ModelResponse,
@@ -725,12 +723,7 @@ async def _map_user_prompt_items(items: Sequence[object]) -> list[ChatCompletion
725723
async def _map_single_item(item: object) -> list[ChatCompletionContentPartParam]:
726724
if isinstance(item, str):
727725
return [ChatCompletionContentPartTextParam(text=item, type='text')]
728-
handled = await OpenAIChatModel._handle_magic_document(item)
729-
if handled is not None:
730-
return handled
731-
handled = await OpenAIChatModel._handle_magic_binary(item)
732-
if handled is not None:
733-
return handled
726+
# Magic* no longer used; logic ported to base handlers
734727
handled = OpenAIChatModel._handle_image_url(item)
735728
if handled is not None:
736729
return handled
@@ -748,55 +741,6 @@ async def _map_single_item(item: object) -> list[ChatCompletionContentPartParam]
748741
# Fallback: unknown type — return empty parts to avoid type-checker Never error
749742
return []
750743

751-
@staticmethod
752-
async def _handle_magic_document(item: object) -> list[ChatCompletionContentPartParam] | None:
753-
if not isinstance(item, MagicDocumentUrl):
754-
return None
755-
if OpenAIChatModel._is_text_like_media_type(item.media_type):
756-
downloaded = await download_item(item, data_format='text', type_format='extension')
757-
filename = item.filename or f'file.{downloaded["data_type"] or "txt"}'
758-
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, downloaded['data'])
759-
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
760-
downloaded_item = await download_item(item, data_format='base64_uri', type_format='extension')
761-
return [
762-
File(
763-
file=FileFile(
764-
file_data=downloaded_item['data'],
765-
filename=f'filename.{downloaded_item["data_type"]}',
766-
),
767-
type='file',
768-
)
769-
]
770-
771-
@staticmethod
772-
async def _handle_magic_binary(item: object) -> list[ChatCompletionContentPartParam] | None:
773-
if not isinstance(item, MagicBinaryContent):
774-
return None
775-
if OpenAIChatModel._is_text_like_media_type(item.media_type):
776-
text = item.data.decode('utf-8')
777-
filename = item.filename or 'file.txt'
778-
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, text)
779-
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
780-
base64_encoded = base64.b64encode(item.data).decode('utf-8')
781-
if item.is_image:
782-
image_url = ImageURL(url=f'data:{item.media_type};base64,{base64_encoded}')
783-
return [ChatCompletionContentPartImageParam(image_url=image_url, type='image_url')]
784-
if item.is_audio:
785-
assert item.format in ('wav', 'mp3')
786-
audio = InputAudio(data=base64_encoded, format=item.format)
787-
return [ChatCompletionContentPartInputAudioParam(input_audio=audio, type='input_audio')]
788-
if item.is_document:
789-
return [
790-
File(
791-
file=FileFile(
792-
file_data=f'data:{item.media_type};base64,{base64_encoded}',
793-
filename=f'filename.{item.format}',
794-
),
795-
type='file',
796-
)
797-
]
798-
raise RuntimeError(f'Unsupported binary content type: {item.media_type}') # pragma: no cover
799-
800744
@staticmethod
801745
def _handle_image_url(item: object) -> list[ChatCompletionContentPartParam] | None:
802746
if not isinstance(item, ImageUrl):
@@ -808,6 +752,27 @@ def _handle_image_url(item: object) -> list[ChatCompletionContentPartParam] | No
808752
async def _handle_binary_content(item: object) -> list[ChatCompletionContentPartParam] | None:
809753
if not isinstance(item, BinaryContent):
810754
return None
755+
if OpenAIChatModel._is_text_like_media_type(item.media_type):
756+
# Inline text-like binary content as a text block
757+
text = item.data.decode('utf-8')
758+
# Derive a sensible default filename from media type
759+
media_type = item.media_type
760+
if media_type == 'text/plain':
761+
filename = 'file.txt'
762+
elif media_type == 'text/csv':
763+
filename = 'file.csv'
764+
elif media_type == 'text/markdown':
765+
filename = 'file.md'
766+
elif media_type == 'application/json' or media_type.endswith('+json'):
767+
filename = 'file.json'
768+
elif media_type == 'application/xml' or media_type.endswith('+xml'):
769+
filename = 'file.xml'
770+
elif media_type in ('application/x-yaml', 'application/yaml', 'text/yaml'):
771+
filename = 'file.yaml'
772+
else:
773+
filename = 'file.txt'
774+
inline = OpenAIChatModel._inline_file_block(filename, media_type, text)
775+
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
811776
base64_encoded = base64.b64encode(item.data).decode('utf-8')
812777
if item.is_image:
813778
image_url = ImageURL(url=f'data:{item.media_type};base64,{base64_encoded}')
@@ -843,6 +808,11 @@ async def _handle_audio_url(item: object) -> list[ChatCompletionContentPartParam
843808
async def _handle_document_url(item: object) -> list[ChatCompletionContentPartParam] | None:
844809
if not isinstance(item, DocumentUrl):
845810
return None
811+
if OpenAIChatModel._is_text_like_media_type(item.media_type):
812+
downloaded_text = await download_item(item, data_format='text', type_format='extension')
813+
filename = f'file.{downloaded_text["data_type"] or "txt"}'
814+
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, downloaded_text['data'])
815+
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
846816
downloaded_item = await download_item(item, data_format='base64_uri', type_format='extension')
847817
return [
848818
File(

0 commit comments

Comments
 (0)