2626from genai_processors import mime_types
2727from google .genai import types as genai_types
2828import PIL .Image
29+ import pydantic
2930
3031
31- class ProcessorPart :
32+ class ProcessorPart ( genai_types . Part ) :
3233 """A wrapper around `Part` with additional metadata.
3334
3435 Represents a single piece of content that can be processed by an agentic
@@ -37,6 +38,10 @@ class ProcessorPart:
3738 Includes metadata such as the producer of the content, the substream the part
3839 belongs to, the MIME type of the content, and arbitrary metadata.
3940 """
41+ _metadata : dict [str , Any ] = pydantic .PrivateAttr (default_factory = dict )
42+ _role : str = pydantic .PrivateAttr (default = '' )
43+ _substream_name : str = pydantic .PrivateAttr (default = '' )
44+ _mimetype : str = pydantic .PrivateAttr (default = '' )
4045
4146 def __init__ (
4247 self ,
@@ -50,48 +55,43 @@ def __init__(
5055 """Constructs a ProcessorPart.
5156
5257 Args:
53- value: The content to use to construct the ProcessorPart.
58+ value: The content to use to construct the ProcessorPart. Any keyword
59+ arguments after this one overrides any properties in value.
5460 role: Optional. The producer of the content. In Genai models, must be
5561 either 'user' or 'model', but the user can set their own semantics.
5662 Useful to set for multi-turn conversations, otherwise can be empty.
5763 substream_name: (Optional) ProcessorPart stream can be split into multiple
5864 independent streams. They may have specific semantics, e.g. a song and
5965 its lyrics, or can be just alternative responses. Prefer using a default
60- substream with an empty name. If the `ProcessorPart` is created using
61- another `ProcessorPart`, this ProcessorPart inherits the existing
62- substream_name, unless it is overridden in this argument.
66+ substream with an empty name.
6367 mimetype: Mime type of the data.
6468 metadata: (Optional) Auxiliary information about the part. If the
6569 `ProcessorPart` is created using another `ProcessorPart` or a
6670 `content_pb2.Part`, this ProcessorPart inherits the existing metadata,
6771 unless it is overridden in this argument.
6872 """
69- super ().__init__ ()
70- self ._metadata = {}
71-
7273 match value :
73- case genai_types .Part ():
74- self ._part = value
7574 case ProcessorPart ():
76- self . _part = value .part
75+ super (). __init__ ( ** value .model_dump ( exclude_unset = True ))
7776 role = role or value .role
7877 substream_name = substream_name or value .substream_name
7978 mimetype = mimetype or value .mimetype
8079 self ._metadata = value .metadata
81- self ._metadata .update (metadata or {})
80+ case genai_types .Part ():
81+ super ().__init__ (** value .model_dump (exclude_unset = True ))
8282 case str ():
83- self . _part = genai_types . Part (text = value )
83+ super (). __init__ (text = value )
8484 case bytes ():
8585 if not mimetype :
8686 raise ValueError (
8787 'MIME type must be specified when constructing a ProcessorPart'
8888 ' from bytes.'
8989 )
9090 if is_text (mimetype ):
91- self . _part = genai_types . Part (text = value .decode ('utf-8' ))
91+ super (). __init__ (text = value .decode ('utf-8' ))
9292 else :
93- self . _part = genai_types . Part . from_bytes (
94- data = value , mime_type = mimetype
93+ super (). __init__ (
94+ inline_data = genai_types . Blob ( data = value , mime_type = mimetype )
9595 )
9696 case PIL .Image .Image ():
9797 if mimetype :
@@ -113,8 +113,10 @@ def __init__(
113113 mimetype = f'image/{ suffix } '
114114 bytes_io = io .BytesIO ()
115115 value .save (bytes_io , suffix .upper ())
116- self ._part = genai_types .Part .from_bytes (
117- data = bytes_io .getvalue (), mime_type = mimetype
116+ super ().__init__ (
117+ inline_data = genai_types .Blob (
118+ data = bytes_io .getvalue (), mime_type = mimetype
119+ )
118120 )
119121 case _:
120122 raise ValueError (f"Can't construct ProcessorPart from { type (value )} ." )
@@ -127,10 +129,10 @@ def __init__(
127129 if mimetype :
128130 self ._mimetype = mimetype
129131 # Otherwise, if MIME type is specified using inline data, use that.
130- elif self ._part . inline_data and self . _part .inline_data .mime_type :
131- self ._mimetype = self ._part . inline_data .mime_type
132+ elif self .inline_data and self .inline_data .mime_type :
133+ self ._mimetype = self .inline_data .mime_type
132134 # Otherwise, if text is not empty, assume 'text/plain' MIME type.
133- elif self ._part . text :
135+ elif self .text :
134136 self ._mimetype = 'text/plain'
135137 else :
136138 self ._mimetype = ''
@@ -144,24 +146,23 @@ def __repr__(self) -> str:
144146 if self .role :
145147 optional_args += f', role={ self .role !r} '
146148 return (
147- f'ProcessorPart({ self .part . to_json_dict ()!r} ,'
149+ f'ProcessorPart({ self .to_json_dict ()!r} ,'
148150 f' mimetype={ self .mimetype !r} { optional_args } )'
149151 )
150152
151153 def __eq__ (self , other : Any ) -> bool :
152154 if not isinstance (other , ProcessorPart ):
153155 return False
154- return (
155- self ._part == other ._part
156- and self ._role .lower () == other ._role .lower ()
157- and self ._substream_name .lower () == other ._substream_name .lower ()
158- and self ._metadata == other ._metadata
159- )
156+ return self .__dict__ == other .__dict__
160157
161158 @property
162159 def part (self ) -> genai_types .Part :
163- """Returns the underlying Genai Part."""
164- return self ._part
160+ """Returns the underlying Genai Part.
161+
162+ DEPRECATED: Use the ProcessorPart itself, it now inherits from genai.Part.
163+ This property is provided for backward compatibility reasons.
164+ """
165+ return self
165166
166167 @property
167168 def role (self ) -> str :
@@ -187,10 +188,10 @@ def bytes(self) -> bytes | None:
187188 Text encoded into bytes or bytes from inline data if the underlying part
188189 is a Blob.
189190 """
190- if self .part . text :
191+ if self .text :
191192 return self .text .encode ()
192- if isinstance (self .part . inline_data , genai_types .Blob ):
193- return self .part . inline_data .data
193+ if isinstance (self .inline_data , genai_types .Blob ):
194+ return self .inline_data .data
194195 return None
195196
196197 @property
@@ -213,25 +214,6 @@ def mimetype(self) -> str:
213214 """
214215 return self ._mimetype or 'text/plain'
215216
216- @property
217- def text (self ) -> str :
218- """Returns part text as string.
219-
220- Returns:
221- The text of the part.
222-
223- Raises:
224- ValueError if part has no text.
225- """
226- if not mime_types .is_text (self .mimetype ):
227- raise ValueError ('Part is not text.' )
228- return self .part .text or ''
229-
230- @text .setter
231- def text (self , value : str ) -> None :
232- """Sets part to a text part."""
233- self ._part = genai_types .Part (text = value )
234-
235217 @property
236218 def metadata (self ) -> dict [str , Any ]:
237219 """Returns metadata."""
@@ -246,16 +228,6 @@ def get_metadata(self, key: str, default=None) -> Any:
246228 """Returns metadata for a given key."""
247229 return self ._metadata .get (key , default )
248230
249- @property
250- def function_call (self ) -> genai_types .FunctionCall | None :
251- """Returns function call."""
252- return self .part .function_call
253-
254- @property
255- def function_response (self ) -> genai_types .FunctionResponse | None :
256- """Returns function response."""
257- return self .part .function_response
258-
259231 @property
260232 def tool_cancellation (self ) -> str | None :
261233 """Returns an id of a function call to be cancelled.
@@ -266,13 +238,13 @@ def tool_cancellation(self) -> str | None:
266238 The id of the function call to be cancelled or None if this part is not a
267239 tool cancellation from the model.
268240 """
269- if not self .part . function_response :
241+ if not self .function_response :
270242 return None
271- if self .part . function_response .name != 'tool_cancellation' :
243+ if self .function_response .name != 'tool_cancellation' :
272244 return None
273- if not self .part . function_response .response :
245+ if not self .function_response .response :
274246 return None
275- return self .part . function_response .response .get ('function_call_id' , None )
247+ return self .function_response .response .get ('function_call_id' , None )
276248
277249 T = TypeVar ('T' )
278250
@@ -301,8 +273,8 @@ def pil_image(self) -> PIL.Image.Image:
301273 if not mime_types .is_image (self .mimetype ):
302274 raise ValueError (f'Part is not an image. Mime type is { self .mimetype } .' )
303275 bytes_io = io .BytesIO ()
304- if self .part . inline_data is not None :
305- bytes_io .write (self .part . inline_data .data )
276+ if self .inline_data is not None :
277+ bytes_io .write (self .inline_data .data )
306278 bytes_io .seek (0 )
307279 return PIL .Image .open (bytes_io )
308280
@@ -474,7 +446,7 @@ def to_dict(self) -> dict[str, Any]:
474446 ```
475447 """
476448 return {
477- 'part' : self .part . model_dump (mode = 'json' , exclude_none = True ),
449+ 'part' : self .model_dump (mode = 'json' , exclude_none = True ),
478450 'role' : self .role ,
479451 'substream_name' : self .substream_name ,
480452 'mimetype' : self .mimetype ,
@@ -848,10 +820,9 @@ def to_genai_contents(
848820 """
849821 processor_content = ProcessorContent (content )
850822 contents = []
851- for role , content_part in itertools .groupby (
823+ for role , content_parts in itertools .groupby (
852824 processor_content , lambda p : p .role
853825 ):
854- content_parts = [p .part for p in content_part ]
855826 contents .append (genai_types .Content (parts = content_parts , role = role ))
856827
857828 return contents
0 commit comments