Skip to content

Commit 01138eb

Browse files
committed
feat: ad conversational agent contracts
1 parent 4937e66 commit 01138eb

11 files changed

Lines changed: 650 additions & 0 deletions

File tree

src/uipath/_cli/_runtime/_contracts.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from opentelemetry.trace import Tracer
2323
from pydantic import BaseModel, Field
2424

25+
from uipath.agent.conversation import UiPathConversationEvent
2526
from uipath.tracing import TracingManager
2627

2728
from ._logging import LogsInterceptor
@@ -134,6 +135,17 @@ def to_dict(self) -> Dict[str, Any]:
134135
return result
135136

136137

138+
class UiPathConversationHandler(ABC):
139+
"""Base delegate for handling UiPath conversation events."""
140+
141+
use_streaming: bool = True
142+
143+
@abstractmethod
144+
def on_event(self, event: UiPathConversationEvent) -> None:
145+
"""Handle a conversation event for a given execution run."""
146+
pass
147+
148+
137149
class UiPathTraceContext(BaseModel):
138150
"""Trace context information for tracing and debugging."""
139151

@@ -173,6 +185,8 @@ class UiPathRuntimeContext(BaseModel):
173185
input_file: Optional[str] = None
174186
is_eval_run: bool = False
175187
log_handler: Optional[logging.Handler] = None
188+
chat_handler: Optional[UiPathConversationHandler] = None
189+
176190
model_config = {"arbitrary_types_allowed": True}
177191

178192
@classmethod
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
)
59+
from .conversation import (
60+
UiPathConversationCapabilities,
61+
UiPathConversationEndEvent,
62+
UiPathConversationStartedEvent,
63+
UiPathConversationStartEvent,
64+
)
65+
from .event import UiPathConversationEvent
66+
from .exchange import (
67+
UiPathConversationExchange,
68+
UiPathConversationExchangeEndEvent,
69+
UiPathConversationExchangeEvent,
70+
UiPathConversationExchangeStartEvent,
71+
)
72+
from .message import (
73+
UiPathConversationMessage,
74+
UiPathConversationMessageEndEvent,
75+
UiPathConversationMessageEvent,
76+
UiPathConversationMessageStartEvent,
77+
)
78+
from .meta import UiPathConversationMetaEvent
79+
from .tool import (
80+
UiPathConversationToolCall,
81+
UiPathConversationToolCallEndEvent,
82+
UiPathConversationToolCallEvent,
83+
UiPathConversationToolCallResult,
84+
UiPathConversationToolCallStartEvent,
85+
)
86+
87+
__all__ = [
88+
# Root
89+
"UiPathConversationEvent",
90+
# Conversation
91+
"UiPathConversationCapabilities",
92+
"UiPathConversationStartEvent",
93+
"UiPathConversationStartedEvent",
94+
"UiPathConversationEndEvent",
95+
# Exchange
96+
"UiPathConversationExchangeStartEvent",
97+
"UiPathConversationExchangeEndEvent",
98+
"UiPathConversationExchangeEvent",
99+
"UiPathConversationExchange",
100+
# Message
101+
"UiPathConversationMessageStartEvent",
102+
"UiPathConversationMessageEndEvent",
103+
"UiPathConversationMessageEvent",
104+
"UiPathConversationMessage",
105+
# Content
106+
"UiPathConversationContentPartChunkEvent",
107+
"UiPathConversationContentPartStartEvent",
108+
"UiPathConversationContentPartEndEvent",
109+
"UiPathConversationContentPartEvent",
110+
"UiPathConversationContentPart",
111+
# Citation
112+
"UiPathConversationCitationStartEvent",
113+
"UiPathConversationCitationEndEvent",
114+
"UiPathConversationCitationEvent",
115+
"UiPathConversationCitationSource",
116+
"UiPathConversationCitationSourceUrl",
117+
"UiPathConversationCitationSourceMedia",
118+
# Tool
119+
"UiPathConversationToolCallStartEvent",
120+
"UiPathConversationToolCallEndEvent",
121+
"UiPathConversationToolCallEvent",
122+
"UiPathConversationToolCallResult",
123+
"UiPathConversationToolCall",
124+
# Async Stream
125+
"UiPathConversationInputStreamChunkEvent",
126+
"UiPathConversationAsyncInputStreamStartEvent",
127+
"UiPathConversationAsyncInputStreamEndEvent",
128+
"UiPathConversationAsyncInputStreamEvent",
129+
# Meta
130+
"UiPathConversationMetaEvent",
131+
]
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Async input stream events."""
2+
3+
from typing import Any, Dict, Optional
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: Optional[int] = 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: Optional[str] = Field(
22+
None, alias="startOfSpeechSensitivity"
23+
)
24+
end_of_speech_sensitivity: Optional[str] = Field(
25+
None, alias="endOfSpeechSensitivity"
26+
)
27+
prefix_padding_ms: Optional[int] = Field(None, alias="prefixPaddingMs")
28+
silence_duration_ms: Optional[int] = Field(None, alias="silenceDurationMs")
29+
meta_data: Optional[Dict[str, Any]] = Field(None, alias="metaData")
30+
31+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
32+
33+
34+
class UiPathConversationAsyncInputStreamEndEvent(BaseModel):
35+
"""Signals the end of an asynchronous input stream."""
36+
37+
meta_data: Optional[Dict[str, Any]] = Field(None, alias="metaData")
38+
last_chunk_content_part_sequence: Optional[int] = Field(
39+
None, alias="lastChunkContentPartSequence"
40+
)
41+
42+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
43+
44+
45+
class UiPathConversationAsyncInputStreamEvent(BaseModel):
46+
"""Encapsulates sub-events related to an asynchronous input stream."""
47+
48+
stream_id: str = Field(..., alias="streamId")
49+
start: Optional[UiPathConversationAsyncInputStreamStartEvent] = None
50+
end: Optional[UiPathConversationAsyncInputStreamEndEvent] = None
51+
chunk: Optional[UiPathConversationInputStreamChunkEvent] = None
52+
meta_event: Optional[Dict[str, Any]] = Field(None, alias="metaEvent")
53+
54+
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, Dict, List, Optional
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: Optional[UiPathConversationCitationStartEvent] = None
25+
end: Optional[UiPathConversationCitationEndEvent] = 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: Optional[str] = Field(None, alias="downloadUrl")
43+
page_number: Optional[str] = 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: Optional[str] = None
52+
53+
# Union of Url or Media
54+
url: Optional[str] = None
55+
mime_type: Optional[str] = Field(None, alias="mimeType")
56+
download_url: Optional[str] = Field(None, alias="downloadUrl")
57+
page_number: Optional[str] = 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)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Message content part events."""
2+
3+
from typing import Any, Dict, List, Optional, Union
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: Optional[int] = Field(None, alias="contentPartSequence")
14+
data: Optional[str] = None
15+
citation: Optional[UiPathConversationCitationEvent] = 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: Optional[Dict[str, Any]] = 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: Optional[int] = Field(
33+
None, alias="lastChunkContentPartSequence"
34+
)
35+
interrupted: Optional[Dict[str, Any]] = None
36+
meta_data: Optional[Dict[str, Any]] = Field(None, alias="metaData")
37+
38+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
39+
40+
41+
class UiPathConversationContentPartEvent(BaseModel):
42+
"""Encapsulates events related to message content parts."""
43+
44+
content_part_id: str = Field(..., alias="contentPartId")
45+
start: Optional[UiPathConversationContentPartStartEvent] = None
46+
end: Optional[UiPathConversationContentPartEndEvent] = None
47+
chunk: Optional[UiPathConversationContentPartChunkEvent] = None
48+
meta_event: Optional[Dict[str, Any]] = Field(None, alias="metaEvent")
49+
50+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
51+
52+
53+
class UiPathInlineValue(BaseModel):
54+
"""Used when a value is small enough to be returned inline."""
55+
56+
inline: Any
57+
58+
59+
class UiPathExternalValue(BaseModel):
60+
"""Used when a value is too large to be returned inline."""
61+
62+
url: str
63+
byte_count: Optional[int] = Field(None, alias="byteCount")
64+
65+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
66+
67+
68+
InlineOrExternal = Union[UiPathInlineValue, UiPathExternalValue]
69+
70+
71+
class UiPathConversationContentPart(BaseModel):
72+
"""Represents a single part of message content."""
73+
74+
content_part_id: str = Field(..., alias="contentPartId")
75+
mime_type: str = Field(..., alias="mimeType")
76+
data: InlineOrExternal
77+
citations: Optional[List[UiPathConversationCitation]] = None
78+
is_transcript: Optional[bool] = Field(None, alias="isTranscript")
79+
is_incomplete: Optional[bool] = Field(None, alias="isIncomplete")
80+
81+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)

0 commit comments

Comments
 (0)