Skip to content

Commit 83b8e15

Browse files
committed
Removed Magic Classes logic implemented directly on BinaryContent and DocumentUrl
1 parent 264a994 commit 83b8e15

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,
@@ -745,12 +743,7 @@ async def _map_user_prompt_items(items: Sequence[object]) -> list[ChatCompletion
745743
async def _map_single_item(item: object) -> list[ChatCompletionContentPartParam]:
746744
if isinstance(item, str):
747745
return [ChatCompletionContentPartTextParam(text=item, type='text')]
748-
handled = await OpenAIChatModel._handle_magic_document(item)
749-
if handled is not None:
750-
return handled
751-
handled = await OpenAIChatModel._handle_magic_binary(item)
752-
if handled is not None:
753-
return handled
746+
# Magic* no longer used; logic ported to base handlers
754747
handled = OpenAIChatModel._handle_image_url(item)
755748
if handled is not None:
756749
return handled
@@ -768,55 +761,6 @@ async def _map_single_item(item: object) -> list[ChatCompletionContentPartParam]
768761
# Fallback: unknown type — return empty parts to avoid type-checker Never error
769762
return []
770763

771-
@staticmethod
772-
async def _handle_magic_document(item: object) -> list[ChatCompletionContentPartParam] | None:
773-
if not isinstance(item, MagicDocumentUrl):
774-
return None
775-
if OpenAIChatModel._is_text_like_media_type(item.media_type):
776-
downloaded = await download_item(item, data_format='text', type_format='extension')
777-
filename = item.filename or f'file.{downloaded["data_type"] or "txt"}'
778-
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, downloaded['data'])
779-
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
780-
downloaded_item = await download_item(item, data_format='base64_uri', type_format='extension')
781-
return [
782-
File(
783-
file=FileFile(
784-
file_data=downloaded_item['data'],
785-
filename=f'filename.{downloaded_item["data_type"]}',
786-
),
787-
type='file',
788-
)
789-
]
790-
791-
@staticmethod
792-
async def _handle_magic_binary(item: object) -> list[ChatCompletionContentPartParam] | None:
793-
if not isinstance(item, MagicBinaryContent):
794-
return None
795-
if OpenAIChatModel._is_text_like_media_type(item.media_type):
796-
text = item.data.decode('utf-8')
797-
filename = item.filename or 'file.txt'
798-
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, text)
799-
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
800-
base64_encoded = base64.b64encode(item.data).decode('utf-8')
801-
if item.is_image:
802-
image_url = ImageURL(url=f'data:{item.media_type};base64,{base64_encoded}')
803-
return [ChatCompletionContentPartImageParam(image_url=image_url, type='image_url')]
804-
if item.is_audio:
805-
assert item.format in ('wav', 'mp3')
806-
audio = InputAudio(data=base64_encoded, format=item.format)
807-
return [ChatCompletionContentPartInputAudioParam(input_audio=audio, type='input_audio')]
808-
if item.is_document:
809-
return [
810-
File(
811-
file=FileFile(
812-
file_data=f'data:{item.media_type};base64,{base64_encoded}',
813-
filename=f'filename.{item.format}',
814-
),
815-
type='file',
816-
)
817-
]
818-
raise RuntimeError(f'Unsupported binary content type: {item.media_type}') # pragma: no cover
819-
820764
@staticmethod
821765
def _handle_image_url(item: object) -> list[ChatCompletionContentPartParam] | None:
822766
if not isinstance(item, ImageUrl):
@@ -828,6 +772,27 @@ def _handle_image_url(item: object) -> list[ChatCompletionContentPartParam] | No
828772
async def _handle_binary_content(item: object) -> list[ChatCompletionContentPartParam] | None:
829773
if not isinstance(item, BinaryContent):
830774
return None
775+
if OpenAIChatModel._is_text_like_media_type(item.media_type):
776+
# Inline text-like binary content as a text block
777+
text = item.data.decode('utf-8')
778+
# Derive a sensible default filename from media type
779+
media_type = item.media_type
780+
if media_type == 'text/plain':
781+
filename = 'file.txt'
782+
elif media_type == 'text/csv':
783+
filename = 'file.csv'
784+
elif media_type == 'text/markdown':
785+
filename = 'file.md'
786+
elif media_type == 'application/json' or media_type.endswith('+json'):
787+
filename = 'file.json'
788+
elif media_type == 'application/xml' or media_type.endswith('+xml'):
789+
filename = 'file.xml'
790+
elif media_type in ('application/x-yaml', 'application/yaml', 'text/yaml'):
791+
filename = 'file.yaml'
792+
else:
793+
filename = 'file.txt'
794+
inline = OpenAIChatModel._inline_file_block(filename, media_type, text)
795+
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
831796
base64_encoded = base64.b64encode(item.data).decode('utf-8')
832797
if item.is_image:
833798
image_url = ImageURL(url=f'data:{item.media_type};base64,{base64_encoded}')
@@ -863,6 +828,11 @@ async def _handle_audio_url(item: object) -> list[ChatCompletionContentPartParam
863828
async def _handle_document_url(item: object) -> list[ChatCompletionContentPartParam] | None:
864829
if not isinstance(item, DocumentUrl):
865830
return None
831+
if OpenAIChatModel._is_text_like_media_type(item.media_type):
832+
downloaded_text = await download_item(item, data_format='text', type_format='extension')
833+
filename = f'file.{downloaded_text["data_type"] or "txt"}'
834+
inline = OpenAIChatModel._inline_file_block(filename, item.media_type, downloaded_text['data'])
835+
return [ChatCompletionContentPartTextParam(text=inline, type='text')]
866836
downloaded_item = await download_item(item, data_format='base64_uri', type_format='extension')
867837
return [
868838
File(

0 commit comments

Comments
 (0)