1515
1616from  collections .abc  import  Hashable 
1717from  dataclasses  import  dataclass , field , replace 
18- from  typing  import  Any , Union 
18+ from  typing  import  Any , Literal ,  Union ,  overload 
1919
20+ from  pydantic_ai ._thinking_part  import  END_THINK_TAG , START_THINK_TAG 
2021from  pydantic_ai .exceptions  import  UnexpectedModelBehavior 
2122from  pydantic_ai .messages  import  (
2223    ModelResponsePart ,
@@ -66,12 +67,30 @@ def get_parts(self) -> list[ModelResponsePart]:
6667        """ 
6768        return  [p  for  p  in  self ._parts  if  not  isinstance (p , ToolCallPartDelta )]
6869
70+     @overload  
6971    def  handle_text_delta (
7072        self ,
7173        * ,
72-         vendor_part_id : Hashable  |  None ,
74+         vendor_part_id : VendorId  |  None ,
7375        content : str ,
74-     ) ->  ModelResponseStreamEvent :
76+     ) ->  ModelResponseStreamEvent : ...
77+ 
78+     @overload  
79+     def  handle_text_delta (
80+         self ,
81+         * ,
82+         vendor_part_id : VendorId ,
83+         content : str ,
84+         extract_think_tags : Literal [True ],
85+     ) ->  ModelResponseStreamEvent  |  None : ...
86+ 
87+     def  handle_text_delta (
88+         self ,
89+         * ,
90+         vendor_part_id : VendorId  |  None ,
91+         content : str ,
92+         extract_think_tags : bool  =  False ,
93+     ) ->  ModelResponseStreamEvent  |  None :
7594        """Handle incoming text content, creating or updating a TextPart in the manager as appropriate. 
7695
7796        When `vendor_part_id` is None, the latest part is updated if it exists and is a TextPart; 
@@ -83,6 +102,7 @@ def handle_text_delta(
83102                of text. If None, a new part will be created unless the latest part is already 
84103                a TextPart. 
85104            content: The text content to append to the appropriate TextPart. 
105+             extract_think_tags: Whether to extract `<think>` tags from the text content and handle them as thinking parts. 
86106
87107        Returns: 
88108            A `PartStartEvent` if a new part was created, or a `PartDeltaEvent` if an existing part was updated. 
@@ -104,9 +124,24 @@ def handle_text_delta(
104124            part_index  =  self ._vendor_id_to_part_index .get (vendor_part_id )
105125            if  part_index  is  not   None :
106126                existing_part  =  self ._parts [part_index ]
107-                 if  not  isinstance (existing_part , TextPart ):
127+ 
128+                 if  extract_think_tags  and  isinstance (existing_part , ThinkingPart ):
129+                     # We may be building a thinking part instead of a text part if we had previously seen a `<think>` tag 
130+                     if  content  ==  END_THINK_TAG :
131+                         # When we see `</think>`, we're done with the thinking part and the next text delta will need a new part 
132+                         self ._vendor_id_to_part_index .pop (vendor_part_id )
133+                         return  None 
134+                     else :
135+                         return  self .handle_thinking_delta (vendor_part_id = vendor_part_id , content = content )
136+                 elif  isinstance (existing_part , TextPart ):
137+                     existing_text_part_and_index  =  existing_part , part_index 
138+                 else :
108139                    raise  UnexpectedModelBehavior (f'Cannot apply a text delta to { existing_part = }  ' )
109-                 existing_text_part_and_index  =  existing_part , part_index 
140+ 
141+         if  extract_think_tags  and  content  ==  START_THINK_TAG :
142+             # When we see a `<think>` tag (which is a single token), we'll build a new thinking part instead 
143+             self ._vendor_id_to_part_index .pop (vendor_part_id , None )
144+             return  self .handle_thinking_delta (vendor_part_id = vendor_part_id , content = '' )
110145
111146        if  existing_text_part_and_index  is  None :
112147            # There is no existing text part that should be updated, so create a new one 
0 commit comments