-
Notifications
You must be signed in to change notification settings - Fork 33
feat: map conversational input to attachments for nodes [JAR-9193] #535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
afc4581
7118afd
79232b9
be6e917
b74002c
7a91213
6a11c20
872ec52
709a97e
37f061a
08bc118
0a933d6
f554378
6340bcb
771d4bd
5582e03
1faf8ba
4ed0bee
9c9a0b6
798c47e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import asyncio | ||
| import json | ||
| import logging | ||
| import uuid | ||
| from datetime import datetime, timezone | ||
| from typing import Any, cast | ||
|
|
||
|
|
@@ -28,6 +29,7 @@ | |
| UiPathConversationToolCallEndEvent, | ||
| UiPathConversationToolCallEvent, | ||
| UiPathConversationToolCallStartEvent, | ||
| UiPathExternalValue, | ||
| UiPathInlineValue, | ||
| ) | ||
| from uipath.runtime import UiPathRuntimeStorageProtocol | ||
|
|
@@ -90,7 +92,6 @@ def map_messages(self, messages: list[Any]) -> list[Any]: | |
| return self._map_messages_internal( | ||
| cast(list[UiPathConversationMessage], messages) | ||
| ) | ||
|
|
||
| # Case3: List[dict] -> parse to List[UiPathConversationMessage] | ||
| if isinstance(first, dict): | ||
| try: | ||
|
|
@@ -118,9 +119,9 @@ def _map_messages_internal( | |
|
|
||
| for uipath_message in messages: | ||
| content_blocks: list[ContentBlock] = [] | ||
| attachments: list[dict[str, Any]] = [] | ||
|
|
||
| # Convert content_parts to content_blocks | ||
| # TODO: Convert file-attachment content-parts to content_blocks as well | ||
| if uipath_message.content_parts: | ||
| for uipath_content_part in uipath_message.content_parts: | ||
| data = uipath_content_part.data | ||
|
|
@@ -134,13 +135,36 @@ def _map_messages_internal( | |
| text, id=uipath_content_part.content_part_id | ||
| ) | ||
| ) | ||
| elif isinstance(data, UiPathExternalValue): | ||
| attachment_id = self.parse_attachment_id_from_content_part_uri( | ||
| data.uri | ||
| ) | ||
| full_name = uipath_content_part.name | ||
| if attachment_id and full_name: | ||
| attachments.append( | ||
| { | ||
| "id": attachment_id, | ||
| "full_name": full_name, | ||
| "mime_type": uipath_content_part.mime_type, | ||
| } | ||
| ) | ||
|
|
||
| # Add attachment references as a text block for LLM visibility | ||
| if attachments: | ||
| content_blocks.append( | ||
| create_text_block( | ||
| f"<uip:attachments>{json.dumps(attachments)}</uip:attachments>" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When the text content_blocks are all combined, any new-lines here should be added? E.g.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The content blocks are still separated in their respective JSON text blocks (handled individually and differently than AIMessage content extraction) e.g. something like
|
||
| ) | ||
| ) | ||
|
|
||
| # Metadata for the user/assistant message | ||
| metadata = { | ||
| metadata: dict[str, Any] = { | ||
| "message_id": uipath_message.message_id, | ||
| "created_at": uipath_message.created_at, | ||
| "updated_at": uipath_message.updated_at, | ||
| } | ||
| if attachments: | ||
| metadata["attachments"] = attachments | ||
|
|
||
| role = uipath_message.role | ||
| if role == "user": | ||
|
|
@@ -244,6 +268,36 @@ def get_timestamp(self): | |
| def get_content_part_id(self, message_id: str) -> str: | ||
| return f"chunk-{message_id}-0" | ||
|
|
||
| def parse_attachment_id_from_content_part_uri(self, uri: str) -> str | None: | ||
| """Parse attachment ID from a URI. | ||
|
|
||
| Extracts the UUID from URIs like: | ||
| "urn:uipath:cas:file:orchestrator:a940a416-b97b-4146-3089-08de5f4d0a87" | ||
|
|
||
| Args: | ||
| uri: The URI to parse | ||
|
|
||
| Returns: | ||
| The attachment ID if found, None otherwise | ||
| """ | ||
| if not uri: | ||
| return None | ||
|
|
||
| # The UUID is the last segment after the final colon | ||
| parts = uri.rsplit(":", 1) | ||
| if len(parts) != 2: | ||
| return None | ||
|
|
||
| potential_uuid = parts[1] | ||
| if not potential_uuid: | ||
| return None | ||
|
|
||
| # Validate it's a proper UUID and normalize to lowercase | ||
| try: | ||
| return str(uuid.UUID(potential_uuid)) | ||
| except (ValueError, AttributeError): | ||
| return None | ||
|
|
||
| async def map_ai_message_chunk_to_events( | ||
| self, message: AIMessageChunk | ||
| ) -> list[UiPathConversationMessageEvent]: | ||
|
|
||

Uh oh!
There was an error while loading. Please reload this page.