66from dataclasses import dataclass
77from functools import cached_property
88
9- from ...agent import AgentDepsT
9+ from typing_extensions import assert_never
10+
1011from ...messages import (
1112 AudioUrl ,
1213 BinaryContent ,
2223 ThinkingPart ,
2324 ToolCallPart ,
2425 ToolReturnPart ,
26+ UserContent ,
2527 UserPromptPart ,
2628 VideoUrl ,
2729)
2830from ...output import OutputDataT
31+ from ...tools import AgentDepsT
2932from ..adapter import UIAdapter
3033from ..event_stream import UIEventStream
3134from ..messages_builder import MessagesBuilder
3639 FileUIPart ,
3740 ReasoningUIPart ,
3841 RequestData ,
42+ SourceDocumentUIPart ,
43+ SourceUrlUIPart ,
44+ StepStartUIPart ,
3945 TextUIPart ,
4046 ToolOutputAvailablePart ,
4147 ToolOutputErrorPart ,
@@ -90,14 +96,16 @@ def load_messages(cls, messages: Sequence[UIMessage]) -> list[ModelMessage]: #
9096 builder = MessagesBuilder ()
9197
9298 for msg in messages :
93- if msg .role in ('system' , 'user' ):
99+ if msg .role == 'system' :
100+ for part in msg .parts :
101+ if isinstance (part , TextUIPart ): # pragma: no branch
102+ builder .add (SystemPromptPart (content = part .text ))
103+ elif msg .role == 'user' :
104+ user_prompt_content : str | list [UserContent ] = []
94105 for part in msg .parts :
95106 if isinstance (part , TextUIPart ):
96- if msg .role == 'system' :
97- builder .add (SystemPromptPart (content = part .text )) # TODO (DouweM): coverage
98- else :
99- builder .add (UserPromptPart (content = part .text ))
100- elif isinstance (part , FileUIPart ): # TODO (DouweM): coverage
107+ user_prompt_content .append (part .text )
108+ elif isinstance (part , FileUIPart ):
101109 try :
102110 file = BinaryContent .from_data_uri (part .url )
103111 except ValueError :
@@ -111,28 +119,30 @@ def load_messages(cls, messages: Sequence[UIMessage]) -> list[ModelMessage]: #
111119 file = AudioUrl (url = part .url , media_type = part .media_type )
112120 case _:
113121 file = DocumentUrl (url = part .url , media_type = part .media_type )
114- builder . add ( UserPromptPart ( content = [ file ]) )
122+ user_prompt_content . append ( file )
115123
116- elif msg .role == 'assistant' : # TODO (DouweM): coverage branch
124+ if user_prompt_content : # pragma: no branch
125+ if len (user_prompt_content ) == 1 and isinstance (user_prompt_content [0 ], str ):
126+ user_prompt_content = user_prompt_content [0 ]
127+ builder .add (UserPromptPart (content = user_prompt_content ))
128+
129+ elif msg .role == 'assistant' :
117130 for part in msg .parts :
118131 if isinstance (part , TextUIPart ):
119132 builder .add (TextPart (content = part .text ))
120133 elif isinstance (part , ReasoningUIPart ):
121- builder .add (ThinkingPart (content = part .text )) # TODO (DouweM): coverage
122- elif isinstance (part , FileUIPart ): # TODO (DouweM): coverage
134+ builder .add (ThinkingPart (content = part .text ))
135+ elif isinstance (part , FileUIPart ):
123136 try :
124137 file = BinaryContent .from_data_uri (part .url )
125- except ValueError as e :
138+ except ValueError as e : # pragma: no cover
126139 # We don't yet handle non-data-URI file URLs returned by assistants, as no Pydantic AI models do this.
127140 raise ValueError (
128141 'Vercel AI integration can currently only handle assistant file parts with data URIs.'
129142 ) from e
130143 builder .add (FilePart (content = file ))
131- elif isinstance (part , DataUIPart ):
132- # Not currently supported
133- pass
134- elif isinstance (part , ToolUIPart | DynamicToolUIPart ): # TODO (DouweM): coverage branch
135- if isinstance (part , DynamicToolUIPart ): # TODO (DouweM): coverage
144+ elif isinstance (part , ToolUIPart | DynamicToolUIPart ):
145+ if isinstance (part , DynamicToolUIPart ):
136146 tool_name = part .tool_name
137147 builtin_tool = False
138148 else :
@@ -142,15 +152,15 @@ def load_messages(cls, messages: Sequence[UIMessage]) -> list[ModelMessage]: #
142152 tool_call_id = part .tool_call_id
143153 args = part .input
144154
145- if builtin_tool : # TODO (DouweM): coverage
155+ if builtin_tool :
146156 call_part = BuiltinToolCallPart (tool_name = tool_name , tool_call_id = tool_call_id , args = args )
147157 builder .add (call_part )
148158
149159 if isinstance (part , ToolOutputAvailablePart | ToolOutputErrorPart ):
150160 if part .state == 'output-available' :
151161 output = part .output
152162 else :
153- output = part .error_text
163+ output = { 'error_text' : part .error_text , 'is_error' : True }
154164
155165 provider_name = (
156166 (part .call_provider_metadata or {}).get ('pydantic_ai' , {}).get ('provider_name' )
@@ -172,11 +182,27 @@ def load_messages(cls, messages: Sequence[UIMessage]) -> list[ModelMessage]: #
172182 builder .add (
173183 ToolReturnPart (tool_name = tool_name , tool_call_id = tool_call_id , content = part .output )
174184 )
175- elif part .state == 'output-error' : # TODO (DouweM): coverage
185+ elif part .state == 'output-error' :
176186 builder .add (
177187 RetryPromptPart (
178188 tool_name = tool_name , tool_call_id = tool_call_id , content = part .error_text
179189 )
180190 )
191+ elif isinstance (part , DataUIPart ):
192+ # Contains custom data that shouldn't be sent to the model
193+ pass
194+ elif isinstance (part , SourceUrlUIPart ):
195+ # TODO: Once we support citations: https://github.com/pydantic/pydantic-ai/issues/3126
196+ pass
197+ elif isinstance (part , SourceDocumentUIPart ):
198+ # TODO: Once we support citations: https://github.com/pydantic/pydantic-ai/issues/3126
199+ pass
200+ elif isinstance (part , StepStartUIPart ):
201+ # Nothing to do here
202+ pass
203+ else :
204+ assert_never (part )
205+ else :
206+ assert_never (msg .role )
181207
182208 return builder .messages
0 commit comments