File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -44,13 +44,9 @@ async def read_file(path: str) -> str | ai.messages.ContentOutput:
4444 data = _resolve_within_allowed (path ).read_bytes ()
4545 image_type = media .detect_image_media_type (data )
4646 if image_type is not None :
47- return ai .messages .ContentOutput (
48- value = [
49- ai .messages .TextPart (
50- text = f"Loaded { path } ({ image_type } , { len (data )} bytes)."
51- ),
52- ai .file_part (data , media_type = image_type ),
53- ]
47+ return ai .content_output (
48+ f"Loaded { path } ({ image_type } , { len (data )} bytes)." ,
49+ ai .file_part (data , media_type = image_type ),
5450 )
5551 return data .decode ("utf-8" , errors = "replace" )
5652
Original file line number Diff line number Diff line change 6363from .types import events , messages , tools
6464from .types .builders import (
6565 assistant_message ,
66+ content_output ,
6667 file_part ,
6768 system_message ,
69+ text_part ,
6870 thinking ,
6971 tool_message ,
7072 tool_result_part ,
119121 "agent" ,
120122 "assistant_message" ,
121123 "cancel_hook" ,
124+ "content_output" ,
122125 "errors" ,
123126 # Submodules
124127 "events" ,
137140 "resolve_hook" ,
138141 "stream" ,
139142 "system_message" ,
143+ "text_part" ,
140144 "thinking" ,
141145 "tool" ,
142146 "tool_message" ,
Original file line number Diff line number Diff line change 1313 from . import events as events_
1414
1515from .messages import (
16+ ContentOutput ,
17+ ContentPart ,
1618 ErrorJsonOutput ,
1719 ErrorTextOutput ,
1820 ExecutionDeniedOutput ,
@@ -92,6 +94,35 @@ def file_part(
9294 return FilePart .from_bytes (data , media_type = media_type , filename = filename )
9395
9496
97+ def text_part (
98+ text : str ,
99+ * ,
100+ provider_metadata : dict [str , Any ] | None = None ,
101+ ) -> TextPart :
102+ """Create a :class:`TextPart`.
103+
104+ Bare strings passed to the ``*_message`` builders are coerced into
105+ text parts automatically; reach for this when you need to attach
106+ ``provider_metadata`` or build a part list directly.
107+ """
108+ return TextPart (text = text , provider_metadata = provider_metadata )
109+
110+
111+ def content_output (* content : str | TextPart | FilePart ) -> ContentOutput :
112+ """Create a multipart :class:`ContentOutput` tool result.
113+
114+ Bare strings become :class:`TextPart` objects, mirroring the
115+ ``*_message`` builders, so a tool can return mixed text and files
116+ without constructing the part list by hand.
117+
118+ >>> ai.content_output("Here is the chart:", ai.file_part(png_bytes))
119+ """
120+ parts : list [ContentPart ] = []
121+ for item in content :
122+ parts .append (TextPart (text = item ) if isinstance (item , str ) else item )
123+ return ContentOutput (value = parts )
124+
125+
95126def thinking (
96127 text : str ,
97128 * ,
Original file line number Diff line number Diff line change @@ -20,6 +20,25 @@ def test_user_message_mixed_content() -> None:
2020 assert isinstance (msg .parts [2 ], messages .TextPart )
2121
2222
23+ def test_text_part () -> None :
24+ tp = builders .text_part ("hello" , provider_metadata = {"k" : "v" })
25+ assert isinstance (tp , messages .TextPart )
26+ assert tp .text == "hello"
27+ assert tp .provider_metadata == {"k" : "v" }
28+
29+
30+ def test_content_output_coerces_strings () -> None :
31+ fp = messages .FilePart (
32+ data = "https://example.com/img.png" , media_type = "image/png"
33+ )
34+ out = builders .content_output ("Here:" , fp )
35+ assert isinstance (out , messages .ContentOutput )
36+ assert len (out .value ) == 2
37+ assert isinstance (out .value [0 ], messages .TextPart )
38+ assert out .value [0 ].text == "Here:"
39+ assert isinstance (out .value [1 ], messages .FilePart )
40+
41+
2342def test_file_part_from_url () -> None :
2443 fp = builders .file_part ("https://example.com/image.png" )
2544 assert isinstance (fp , messages .FilePart )
You can’t perform that action at this time.
0 commit comments