Skip to content

Commit affe38a

Browse files
committed
Removed Magic Classes logic implemented directly on BinaryContent and DocumentUrl
1 parent a1741fa commit affe38a

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,
@@ -734,12 +732,7 @@ async def _map_user_prompt_items(items: Sequence[object]) -> list[ChatCompletion
734732
async def _map_single_item(item: object) -> list[ChatCompletionContentPartParam]:
735733
if isinstance(item, str):
736734
return [ChatCompletionContentPartTextParam(text=item, type='text')]
737-
handled = await OpenAIChatModel._handle_magic_document(item)
738-
if handled is not None:
739-
return handled
740-
handled = await OpenAIChatModel._handle_magic_binary(item)
741-
if handled is not None:
742-
return handled
735+
# Magic* no longer used; logic ported to base handlers
743736
handled = OpenAIChatModel._handle_image_url(item)
744737
if handled is not None:
745738
return handled
@@ -757,55 +750,6 @@ async def _map_single_item(item: object) -> list[ChatCompletionContentPartParam]
757750
# Fallback: unknown type — return empty parts to avoid type-checker Never error
758751
return []
759752

760-
@staticmethod
761-
async def _handle_magic_document(item: object) -> list[ChatCompletionContentPartParam] | None:
762-
if not isinstance(item, MagicDocumentUrl):
763-
return None
764-
if OpenAIChatModel._is_text_like_media_type(item.media_type):
765-
downloaded = await download_item(item, data_format='text', type_format='extension')
766-
filename = item.filename or f'file.{downloaded["data_type"] or "txt"}'
767-
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, downloaded['data'])
768-
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
769-
downloaded_item = await download_item(item, data_format='base64_uri', type_format='extension')
770-
return [
771-
File(
772-
file=FileFile(
773-
file_data=downloaded_item['data'],
774-
filename=f'filename.{downloaded_item["data_type"]}',
775-
),
776-
type='file',
777-
)
778-
]
779-
780-
@staticmethod
781-
async def _handle_magic_binary(item: object) -> list[ChatCompletionContentPartParam] | None:
782-
if not isinstance(item, MagicBinaryContent):
783-
return None
784-
if OpenAIChatModel._is_text_like_media_type(item.media_type):
785-
text = item.data.decode('utf-8')
786-
filename = item.filename or 'file.txt'
787-
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, text)
788-
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
789-
base64_encoded = base64.b64encode(item.data).decode('utf-8')
790-
if item.is_image:
791-
image_url = ImageURL(url=f'data:{item.media_type};base64,{base64_encoded}')
792-
return [ChatCompletionContentPartImageParam(image_url=image_url, type='image_url')]
793-
if item.is_audio:
794-
assert item.format in ('wav', 'mp3')
795-
audio = InputAudio(data=base64_encoded, format=item.format)
796-
return [ChatCompletionContentPartInputAudioParam(input_audio=audio, type='input_audio')]
797-
if item.is_document:
798-
return [
799-
File(
800-
file=FileFile(
801-
file_data=f'data:{item.media_type};base64,{base64_encoded}',
802-
filename=f'filename.{item.format}',
803-
),
804-
type='file',
805-
)
806-
]
807-
raise RuntimeError(f'Unsupported binary content type: {item.media_type}') # pragma: no cover
808-
809753
@staticmethod
810754
def _handle_image_url(item: object) -> list[ChatCompletionContentPartParam] | None:
811755
if not isinstance(item, ImageUrl):
@@ -817,6 +761,27 @@ def _handle_image_url(item: object) -> list[ChatCompletionContentPartParam] | No
817761
async def _handle_binary_content(item: object) -> list[ChatCompletionContentPartParam] | None:
818762
if not isinstance(item, BinaryContent):
819763
return None
764+
if OpenAIChatModel._is_text_like_media_type(item.media_type):
765+
# Inline text-like binary content as a text block
766+
text = item.data.decode('utf-8')
767+
# Derive a sensible default filename from media type
768+
media_type = item.media_type
769+
if media_type == 'text/plain':
770+
filename = 'file.txt'
771+
elif media_type == 'text/csv':
772+
filename = 'file.csv'
773+
elif media_type == 'text/markdown':
774+
filename = 'file.md'
775+
elif media_type == 'application/json' or media_type.endswith('+json'):
776+
filename = 'file.json'
777+
elif media_type == 'application/xml' or media_type.endswith('+xml'):
778+
filename = 'file.xml'
779+
elif media_type in ('application/x-yaml', 'application/yaml', 'text/yaml'):
780+
filename = 'file.yaml'
781+
else:
782+
filename = 'file.txt'
783+
inline = OpenAIChatModel._inline_file_block(filename, media_type, text)
784+
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
820785
base64_encoded = base64.b64encode(item.data).decode('utf-8')
821786
if item.is_image:
822787
image_url = ImageURL(url=f'data:{item.media_type};base64,{base64_encoded}')
@@ -852,6 +817,11 @@ async def _handle_audio_url(item: object) -> list[ChatCompletionContentPartParam
852817
async def _handle_document_url(item: object) -> list[ChatCompletionContentPartParam] | None:
853818
if not isinstance(item, DocumentUrl):
854819
return None
820+
if OpenAIChatModel._is_text_like_media_type(item.media_type):
821+
downloaded_text = await download_item(item, data_format='text', type_format='extension')
822+
filename = f'file.{downloaded_text["data_type"] or "txt"}'
823+
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, downloaded_text['data'])
824+
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
855825
downloaded_item = await download_item(item, data_format='base64_uri', type_format='extension')
856826
return [
857827
File(

0 commit comments

Comments
 (0)