diff --git a/packages/uipath-core/pyproject.toml b/packages/uipath-core/pyproject.toml index 959122a11..147726394 100644 --- a/packages/uipath-core/pyproject.toml +++ b/packages/uipath-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" description = "UiPath Core abstractions" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/packages/uipath-core/src/uipath/core/chat/content.py b/packages/uipath-core/src/uipath/core/chat/content.py index cc6300490..4ae8b169c 100644 --- a/packages/uipath-core/src/uipath/core/chat/content.py +++ b/packages/uipath-core/src/uipath/core/chat/content.py @@ -2,6 +2,7 @@ from __future__ import annotations +import uuid from typing import Any, Sequence from pydantic import BaseModel, ConfigDict, Field @@ -95,7 +96,7 @@ class UiPathConversationContentPartData(BaseModel): mime_type: str = Field(..., alias="mimeType") data: InlineOrExternal - citations: Sequence[UiPathConversationCitationData] + citations: Sequence[UiPathConversationCitationData] = Field(default_factory=list) is_transcript: bool | None = Field(None, alias="isTranscript") is_incomplete: bool | None = Field(None, alias="isIncomplete") name: str | None = None @@ -106,11 +107,13 @@ class UiPathConversationContentPartData(BaseModel): class UiPathConversationContentPart(UiPathConversationContentPartData): """Represents a single part of message content.""" - content_part_id: str = Field(..., alias="contentPartId") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") + content_part_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), alias="contentPartId" + ) + created_at: str | None = Field(None, alias="createdAt") + updated_at: str | None = Field(None, alias="updatedAt") # Override to use full type - citations: Sequence[UiPathConversationCitation] + citations: Sequence[UiPathConversationCitation] = Field(default_factory=list) model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/src/uipath/core/chat/message.py b/packages/uipath-core/src/uipath/core/chat/message.py index 9d6aa248d..37aa2bd76 100644 --- a/packages/uipath-core/src/uipath/core/chat/message.py +++ b/packages/uipath-core/src/uipath/core/chat/message.py @@ -1,5 +1,6 @@ """Message-level events.""" +import uuid from typing import Any, Sequence from pydantic import BaseModel, ConfigDict, Field @@ -61,7 +62,9 @@ class UiPathConversationMessageData(BaseModel): content_parts: Sequence[UiPathConversationContentPartData] = Field( ..., alias="contentParts" ) - tool_calls: Sequence[UiPathConversationToolCallData] = Field(..., alias="toolCalls") + tool_calls: Sequence[UiPathConversationToolCallData] = Field( + default_factory=list, alias="toolCalls" + ) model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) @@ -69,15 +72,19 @@ class UiPathConversationMessageData(BaseModel): class UiPathConversationMessage(UiPathConversationMessageData): """Represents a single message within an exchange.""" - message_id: str = Field(..., alias="messageId") - created_at: str = Field(..., alias="createdAt") - updated_at: str = Field(..., alias="updatedAt") + message_id: str = Field( + default_factory=lambda: str(uuid.uuid4()), alias="messageId" + ) + created_at: str | None = Field(None, alias="createdAt") + updated_at: str | None = Field(None, alias="updatedAt") span_id: str | None = Field(None, alias="spanId") # Overrides to use full types content_parts: Sequence[UiPathConversationContentPart] = Field( ..., alias="contentParts" ) - tool_calls: Sequence[UiPathConversationToolCall] = Field(..., alias="toolCalls") + tool_calls: Sequence[UiPathConversationToolCall] = Field( + default_factory=list, alias="toolCalls" + ) model_config = ConfigDict(validate_by_name=True, validate_by_alias=True) diff --git a/packages/uipath-core/tests/chat/__init__.py b/packages/uipath-core/tests/chat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/uipath-core/tests/chat/test_message.py b/packages/uipath-core/tests/chat/test_message.py new file mode 100644 index 000000000..113d34a66 --- /dev/null +++ b/packages/uipath-core/tests/chat/test_message.py @@ -0,0 +1,59 @@ +"""Tests for `UiPathConversationMessage` input validation. + +Conversational Agent Service contract treats `role` + `contentParts` as +the load-bearing fields for an inbound user message. `messageId` and +`contentPartId` are GUIDs that identify entities in the conversation +hierarchy; when omitted on input, the model fills them with fresh +UUIDs (matching what `uipath dev` does server-side). `createdAt`, +`updatedAt`, `spanId`, and `toolCalls` are server-allocated and absent +from client input. + +These tests pin that behavior so `--input-file` payloads from +`uip codedagent run` validate against the model without requiring +callers to hand-generate UUIDs. +""" + +from __future__ import annotations + +from uipath.core.chat import UiPathConversationMessage + + +def test_minimal_user_message_validates_and_fills_ids() -> None: + msg = UiPathConversationMessage.model_validate( + { + "role": "user", + "contentParts": [ + { + "mimeType": "text/plain", + "data": {"inline": "hello world"}, + } + ], + } + ) + assert msg.role == "user" + assert msg.tool_calls == [] + assert msg.created_at is None + assert msg.updated_at is None + assert msg.message_id # auto-generated UUID + assert msg.content_parts[0].content_part_id # auto-generated UUID + assert msg.content_parts[0].citations == [] + + +def test_explicit_ids_are_preserved() -> None: + msg = UiPathConversationMessage.model_validate( + { + "messageId": "00000000-0000-0000-0000-000000000001", + "role": "user", + "contentParts": [ + { + "contentPartId": "00000000-0000-0000-0000-000000000002", + "mimeType": "text/plain", + "data": {"inline": "hello world"}, + } + ], + } + ) + assert msg.message_id == "00000000-0000-0000-0000-000000000001" + assert ( + msg.content_parts[0].content_part_id == "00000000-0000-0000-0000-000000000002" + ) diff --git a/packages/uipath-core/uv.lock b/packages/uipath-core/uv.lock index ab6e6aa14..9b043599c 100644 --- a/packages/uipath-core/uv.lock +++ b/packages/uipath-core/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-20T15:11:06.1716446Z" exclude-newer-span = "P2D" [[package]] @@ -1011,7 +1011,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" source = { editable = "." } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath-platform/uv.lock b/packages/uipath-platform/uv.lock index 53728076b..c3b60638d 100644 --- a/packages/uipath-platform/uv.lock +++ b/packages/uipath-platform/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-20T15:43:10.4544027Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -1063,7 +1063,7 @@ wheels = [ [[package]] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" }, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index d29f9a146..449f50872 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-05-20T15:14:33.9075119Z" exclude-newer-span = "P2D" [options.exclude-newer-package] @@ -2659,7 +2659,7 @@ dev = [ [[package]] name = "uipath-core" -version = "0.5.15" +version = "0.5.16" source = { editable = "../uipath-core" } dependencies = [ { name = "opentelemetry-instrumentation" },