11import asyncio
22import json
33import logging
4+ import uuid
45from datetime import datetime , timezone
56from typing import Any , cast
67
2829 UiPathConversationToolCallEndEvent ,
2930 UiPathConversationToolCallEvent ,
3031 UiPathConversationToolCallStartEvent ,
32+ UiPathExternalValue ,
3133 UiPathInlineValue ,
3234)
3335from uipath .runtime import UiPathRuntimeStorageProtocol
@@ -90,7 +92,6 @@ def map_messages(self, messages: list[Any]) -> list[Any]:
9092 return self ._map_messages_internal (
9193 cast (list [UiPathConversationMessage ], messages )
9294 )
93-
9495 # Case3: List[dict] -> parse to List[UiPathConversationMessage]
9596 if isinstance (first , dict ):
9697 try :
@@ -118,9 +119,9 @@ def _map_messages_internal(
118119
119120 for uipath_message in messages :
120121 content_blocks : list [ContentBlock ] = []
122+ attachments : list [dict [str , Any ]] = []
121123
122124 # Convert content_parts to content_blocks
123- # TODO: Convert file-attachment content-parts to content_blocks as well
124125 if uipath_message .content_parts :
125126 for uipath_content_part in uipath_message .content_parts :
126127 data = uipath_content_part .data
@@ -134,13 +135,36 @@ def _map_messages_internal(
134135 text , id = uipath_content_part .content_part_id
135136 )
136137 )
138+ elif isinstance (data , UiPathExternalValue ):
139+ attachment_id = self .parse_attachment_id_from_content_part_uri (
140+ data .uri
141+ )
142+ full_name = uipath_content_part .name
143+ if attachment_id and full_name :
144+ attachments .append (
145+ {
146+ "id" : attachment_id ,
147+ "full_name" : full_name ,
148+ "mime_type" : uipath_content_part .mime_type ,
149+ }
150+ )
151+
152+ # Add attachment references as a text block for LLM visibility
153+ if attachments :
154+ content_blocks .append (
155+ create_text_block (
156+ f"<uip:attachments>{ json .dumps (attachments )} </uip:attachments>"
157+ )
158+ )
137159
138160 # Metadata for the user/assistant message
139- metadata = {
161+ metadata : dict [ str , Any ] = {
140162 "message_id" : uipath_message .message_id ,
141163 "created_at" : uipath_message .created_at ,
142164 "updated_at" : uipath_message .updated_at ,
143165 }
166+ if attachments :
167+ metadata ["attachments" ] = attachments
144168
145169 role = uipath_message .role
146170 if role == "user" :
@@ -244,6 +268,36 @@ def get_timestamp(self):
244268 def get_content_part_id (self , message_id : str ) -> str :
245269 return f"chunk-{ message_id } -0"
246270
271+ def parse_attachment_id_from_content_part_uri (self , uri : str ) -> str | None :
272+ """Parse attachment ID from a URI.
273+
274+ Extracts the UUID from URIs like:
275+ "urn:uipath:cas:file:orchestrator:a940a416-b97b-4146-3089-08de5f4d0a87"
276+
277+ Args:
278+ uri: The URI to parse
279+
280+ Returns:
281+ The attachment ID if found, None otherwise
282+ """
283+ if not uri :
284+ return None
285+
286+ # The UUID is the last segment after the final colon
287+ parts = uri .rsplit (":" , 1 )
288+ if len (parts ) != 2 :
289+ return None
290+
291+ potential_uuid = parts [1 ]
292+ if not potential_uuid :
293+ return None
294+
295+ # Validate it's a proper UUID and normalize to lowercase
296+ try :
297+ return str (uuid .UUID (potential_uuid ))
298+ except (ValueError , AttributeError ):
299+ return None
300+
247301 async def map_ai_message_chunk_to_events (
248302 self , message : AIMessageChunk
249303 ) -> list [UiPathConversationMessageEvent ]:
0 commit comments