Skip to content

Commit 9ce5a2c

Browse files
committed
fix: add chat streaming models
1 parent e824efd commit 9ce5a2c

File tree

12 files changed

+633
-2
lines changed

12 files changed

+633
-2
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-runtime"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""UiPath Conversation Models.
2+
3+
This module provides Pydantic models that represent the JSON event schema for conversations between a client (UI) and an LLM/agent.
4+
5+
The event objects define a hierarchal conversation structure:
6+
7+
* Conversation
8+
* Exchange
9+
* Message
10+
* Content Parts
11+
* Citations
12+
* Tool Calls
13+
* Tool Results
14+
15+
A conversation may contain multiple exchanges, and an exchange may contain multiple messages. A message may contain
16+
multiple content parts, each of which can be text or binary, including media input and output streams; and each
17+
content part can include multiple citations. A message may also contain multiple tool calls, which may contain a tool
18+
result.
19+
20+
The protocol also supports a top level, "async", input media streams (audio and video), which can span multiple
21+
exchanges. These are used for Gemini's automatic turn detection mode, where the LLM determines when the user has
22+
stopped talking and starts producing output. The output forms one or more messages in an exchange with no explicit
23+
input message. However, the LLM may produce an input transcript which can be used to construct the implicit input
24+
message that started the exchange.
25+
26+
In addition, the protocol also supports "async" tool calls that span multiple exchanges. This can be used with
27+
Gemini's asynchronous function calling protocol, which allows function calls to produce results that interrupt the
28+
conversation when ready, even after multiple exchanges. They also support generating multiple results from a single
29+
tool call. By contrast most tool calls are scoped to a single message, which contains both the call and the single
30+
result produced by that call.
31+
32+
Not all features supported by the protocol will be supported by all clients and LLMs. The optional top level
33+
`capabilities` property can be used to communicate information about supported features. This property should be set
34+
on the first event written to a new websocket connection. This initial event may or may not contain additional
35+
sub-events.
36+
"""
37+
38+
from .async_stream import (
39+
UiPathConversationAsyncInputStreamEndEvent,
40+
UiPathConversationAsyncInputStreamEvent,
41+
UiPathConversationAsyncInputStreamStartEvent,
42+
UiPathConversationInputStreamChunkEvent,
43+
)
44+
from .citation import (
45+
UiPathConversationCitationEndEvent,
46+
UiPathConversationCitationEvent,
47+
UiPathConversationCitationSource,
48+
UiPathConversationCitationSourceMedia,
49+
UiPathConversationCitationSourceUrl,
50+
UiPathConversationCitationStartEvent,
51+
)
52+
from .content import (
53+
UiPathConversationContentPart,
54+
UiPathConversationContentPartChunkEvent,
55+
UiPathConversationContentPartEndEvent,
56+
UiPathConversationContentPartEvent,
57+
UiPathConversationContentPartStartEvent,
58+
UiPathExternalValue,
59+
UiPathInlineValue,
60+
)
61+
from .conversation import (
62+
UiPathConversationCapabilities,
63+
UiPathConversationEndEvent,
64+
UiPathConversationStartedEvent,
65+
UiPathConversationStartEvent,
66+
)
67+
from .event import UiPathConversationEvent
68+
from .exchange import (
69+
UiPathConversationExchange,
70+
UiPathConversationExchangeEndEvent,
71+
UiPathConversationExchangeEvent,
72+
UiPathConversationExchangeStartEvent,
73+
)
74+
from .message import (
75+
UiPathConversationMessage,
76+
UiPathConversationMessageEndEvent,
77+
UiPathConversationMessageEvent,
78+
UiPathConversationMessageStartEvent,
79+
)
80+
from .meta import UiPathConversationMetaEvent
81+
from .tool import (
82+
UiPathConversationToolCall,
83+
UiPathConversationToolCallEndEvent,
84+
UiPathConversationToolCallEvent,
85+
UiPathConversationToolCallResult,
86+
UiPathConversationToolCallStartEvent,
87+
)
88+
89+
__all__ = [
90+
# Root
91+
"UiPathConversationEvent",
92+
# Conversation
93+
"UiPathConversationCapabilities",
94+
"UiPathConversationStartEvent",
95+
"UiPathConversationStartedEvent",
96+
"UiPathConversationEndEvent",
97+
# Exchange
98+
"UiPathConversationExchangeStartEvent",
99+
"UiPathConversationExchangeEndEvent",
100+
"UiPathConversationExchangeEvent",
101+
"UiPathConversationExchange",
102+
# Message
103+
"UiPathConversationMessageStartEvent",
104+
"UiPathConversationMessageEndEvent",
105+
"UiPathConversationMessageEvent",
106+
"UiPathConversationMessage",
107+
# Content
108+
"UiPathConversationContentPartChunkEvent",
109+
"UiPathConversationContentPartStartEvent",
110+
"UiPathConversationContentPartEndEvent",
111+
"UiPathConversationContentPartEvent",
112+
"UiPathConversationContentPart",
113+
"UiPathInlineValue",
114+
"UiPathExternalValue",
115+
# Citation
116+
"UiPathConversationCitationStartEvent",
117+
"UiPathConversationCitationEndEvent",
118+
"UiPathConversationCitationEvent",
119+
"UiPathConversationCitationSource",
120+
"UiPathConversationCitationSourceUrl",
121+
"UiPathConversationCitationSourceMedia",
122+
# Tool
123+
"UiPathConversationToolCallStartEvent",
124+
"UiPathConversationToolCallEndEvent",
125+
"UiPathConversationToolCallEvent",
126+
"UiPathConversationToolCallResult",
127+
"UiPathConversationToolCall",
128+
# Async Stream
129+
"UiPathConversationInputStreamChunkEvent",
130+
"UiPathConversationAsyncInputStreamStartEvent",
131+
"UiPathConversationAsyncInputStreamEndEvent",
132+
"UiPathConversationAsyncInputStreamEvent",
133+
# Meta
134+
"UiPathConversationMetaEvent",
135+
]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Async input stream events."""
2+
3+
from typing import Any
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
8+
class UiPathConversationInputStreamChunkEvent(BaseModel):
9+
"""Represents a single chunk of input stream data."""
10+
11+
input_stream_sequence: int | None = Field(None, alias="inputStreamSequence")
12+
data: str
13+
14+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
15+
16+
17+
class UiPathConversationAsyncInputStreamStartEvent(BaseModel):
18+
"""Signals the start of an asynchronous input stream."""
19+
20+
mime_type: str = Field(..., alias="mimeType")
21+
start_of_speech_sensitivity: str | None = Field(
22+
None, alias="startOfSpeechSensitivity"
23+
)
24+
end_of_speech_sensitivity: str | None = Field(None, alias="endOfSpeechSensitivity")
25+
prefix_padding_ms: int | None = Field(None, alias="prefixPaddingMs")
26+
silence_duration_ms: int | None = Field(None, alias="silenceDurationMs")
27+
meta_data: dict[str, Any] | None = Field(None, alias="metaData")
28+
29+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
30+
31+
32+
class UiPathConversationAsyncInputStreamEndEvent(BaseModel):
33+
"""Signals the end of an asynchronous input stream."""
34+
35+
meta_data: dict[str, Any] | None = Field(None, alias="metaData")
36+
last_chunk_content_part_sequence: int | None = Field(
37+
None, alias="lastChunkContentPartSequence"
38+
)
39+
40+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
41+
42+
43+
class UiPathConversationAsyncInputStreamEvent(BaseModel):
44+
"""Encapsulates sub-events related to an asynchronous input stream."""
45+
46+
stream_id: str = Field(..., alias="streamId")
47+
start: UiPathConversationAsyncInputStreamStartEvent | None = None
48+
end: UiPathConversationAsyncInputStreamEndEvent | None = None
49+
chunk: UiPathConversationInputStreamChunkEvent | None = None
50+
meta_event: dict[str, Any] | None = Field(None, alias="metaEvent")
51+
52+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Citation events for message content."""
2+
3+
from typing import Any
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
8+
class UiPathConversationCitationStartEvent(BaseModel):
9+
"""Indicates the start of a citation target in a content part."""
10+
11+
pass
12+
13+
14+
class UiPathConversationCitationEndEvent(BaseModel):
15+
"""Indicates the end of a citation target in a content part."""
16+
17+
sources: list[dict[str, Any]]
18+
19+
20+
class UiPathConversationCitationEvent(BaseModel):
21+
"""Encapsulates sub-events related to citations."""
22+
23+
citation_id: str = Field(..., alias="citationId")
24+
start: UiPathConversationCitationStartEvent | None = None
25+
end: UiPathConversationCitationEndEvent | None = None
26+
27+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
28+
29+
30+
class UiPathConversationCitationSourceUrl(BaseModel):
31+
"""Represents a citation source that can be rendered as a link (URL)."""
32+
33+
url: str
34+
35+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
36+
37+
38+
class UiPathConversationCitationSourceMedia(BaseModel):
39+
"""Represents a citation source that references media, such as a PDF document."""
40+
41+
mime_type: str = Field(..., alias="mimeType")
42+
download_url: str | None = Field(None, alias="downloadUrl")
43+
page_number: str | None = Field(None, alias="pageNumber")
44+
45+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
46+
47+
48+
class UiPathConversationCitationSource(BaseModel):
49+
"""Represents a citation source, either a URL or media reference."""
50+
51+
title: str | None = None
52+
53+
# Union of Url or Media
54+
url: str | None = None
55+
mime_type: str | None = Field(None, alias="mimeType")
56+
download_url: str | None = Field(None, alias="downloadUrl")
57+
page_number: str | None = Field(None, alias="pageNumber")
58+
59+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
60+
61+
62+
class UiPathConversationCitation(BaseModel):
63+
"""Represents a citation or reference inside a content part."""
64+
65+
citation_id: str = Field(..., alias="citationId")
66+
offset: int
67+
length: int
68+
sources: list[UiPathConversationCitationSource]
69+
70+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)

src/uipath/runtime/chat/content.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Message content part events."""
2+
3+
from typing import Any
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
from .citation import UiPathConversationCitation, UiPathConversationCitationEvent
8+
9+
10+
class UiPathConversationContentPartChunkEvent(BaseModel):
11+
"""Contains a chunk of a message content part."""
12+
13+
content_part_sequence: int | None = Field(None, alias="contentPartSequence")
14+
data: str | None = None
15+
citation: UiPathConversationCitationEvent | None = None
16+
17+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
18+
19+
20+
class UiPathConversationContentPartStartEvent(BaseModel):
21+
"""Signals the start of a message content part."""
22+
23+
mime_type: str = Field(..., alias="mimeType")
24+
meta_data: dict[str, Any] | None = Field(None, alias="metaData")
25+
26+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
27+
28+
29+
class UiPathConversationContentPartEndEvent(BaseModel):
30+
"""Signals the end of a message content part."""
31+
32+
last_chunk_content_part_sequence: int | None = Field(
33+
None, alias="lastChunkContentPartSequence"
34+
)
35+
interrupted: dict[str, Any] | None = None
36+
meta_data: dict[str, Any] | None = Field(None, alias="metaData")
37+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
38+
39+
40+
class UiPathConversationContentPartEvent(BaseModel):
41+
"""Encapsulates events related to message content parts."""
42+
43+
content_part_id: str = Field(..., alias="contentPartId")
44+
start: UiPathConversationContentPartStartEvent | None = None
45+
end: UiPathConversationContentPartEndEvent | None = None
46+
chunk: UiPathConversationContentPartChunkEvent | None = None
47+
meta_event: dict[str, Any] | None = Field(None, alias="metaEvent")
48+
49+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
50+
51+
52+
class UiPathInlineValue(BaseModel):
53+
"""Used when a value is small enough to be returned inline."""
54+
55+
inline: Any
56+
57+
58+
class UiPathExternalValue(BaseModel):
59+
"""Used when a value is too large to be returned inline."""
60+
61+
url: str
62+
byte_count: int | None = Field(None, alias="byteCount")
63+
64+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
65+
66+
67+
InlineOrExternal = UiPathInlineValue | UiPathExternalValue
68+
69+
70+
class UiPathConversationContentPart(BaseModel):
71+
"""Represents a single part of message content."""
72+
73+
content_part_id: str = Field(..., alias="contentPartId")
74+
mime_type: str = Field(..., alias="mimeType")
75+
data: InlineOrExternal
76+
citations: list[UiPathConversationCitation] | None = None
77+
is_transcript: bool | None = Field(None, alias="isTranscript")
78+
is_incomplete: bool | None = Field(None, alias="isIncomplete")
79+
80+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Conversation-level events and capabilities."""
2+
3+
from typing import Any
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
8+
class UiPathConversationCapabilities(BaseModel):
9+
"""Describes the capabilities of a conversation participant."""
10+
11+
async_input_stream_emitter: bool | None = Field(
12+
None, alias="asyncInputStreamEmitter"
13+
)
14+
async_input_stream_handler: bool | None = Field(
15+
None, alias="asyncInputStreamHandler"
16+
)
17+
async_tool_call_emitter: bool | None = Field(None, alias="asyncToolCallEmitter")
18+
async_tool_call_handler: bool | None = Field(None, alias="asyncToolCallHandler")
19+
mime_types_emitted: list[str] | None = Field(None, alias="mimeTypesEmitted")
20+
mime_types_handled: list[str] | None = Field(None, alias="mimeTypesHandled")
21+
22+
model_config = ConfigDict(
23+
validate_by_name=True, validate_by_alias=True, extra="allow"
24+
)
25+
26+
27+
class UiPathConversationStartEvent(BaseModel):
28+
"""Signals the start of a conversation event stream."""
29+
30+
capabilities: UiPathConversationCapabilities | None = None
31+
meta_data: dict[str, Any] | None = Field(None, alias="metaData")
32+
33+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
34+
35+
36+
class UiPathConversationStartedEvent(BaseModel):
37+
"""Signals the acceptance of the start of a conversation."""
38+
39+
capabilities: UiPathConversationCapabilities | None = None
40+
41+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
42+
43+
44+
class UiPathConversationEndEvent(BaseModel):
45+
"""Signals the end of a conversation event stream."""
46+
47+
meta_data: dict[str, Any] | None = Field(None, alias="metaData")
48+
49+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)

0 commit comments

Comments
 (0)