Skip to content

Commit 0a10c27

Browse files
feat: initial support for conversational agents
1 parent da96fc8 commit 0a10c27

File tree

9 files changed

+245
-0
lines changed

9 files changed

+245
-0
lines changed

src/uipath/_services/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .buckets_service import BucketsService
66
from .connections_service import ConnectionsService
77
from .context_grounding_service import ContextGroundingService
8+
from .conversations_service import ConversationsService
89
from .documents_service import DocumentsService
910
from .entities_service import EntitiesService
1011
from .external_application_service import ExternalApplicationService
@@ -31,4 +32,5 @@
3132
"FolderService",
3233
"EntitiesService",
3334
"ExternalApplicationService",
35+
"ConversationsService",
3436
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from .._config import Config
2+
from .._execution_context import ExecutionContext
3+
from .._utils import Endpoint, RequestSpec
4+
from ..agent.conversation import UiPathConversationMessage
5+
from ..tracing import traced
6+
from ._base_service import BaseService
7+
8+
9+
class ConversationsService(BaseService):
10+
def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
11+
super().__init__(config=config, execution_context=execution_context)
12+
13+
@traced(name="retrieve_latest_exchange_message", run_type="uipath")
14+
async def retrieve_latest_exchange_message_async(
15+
self, conversation_id: str, exchange_id: str, message_id: str
16+
) -> UiPathConversationMessage:
17+
retrieve_message_spec = self._retrieve_latest_exchange_message_spec(
18+
conversation_id, exchange_id, message_id
19+
)
20+
21+
response = await self.request_async(
22+
retrieve_message_spec.method, retrieve_message_spec.endpoint
23+
)
24+
25+
return UiPathConversationMessage.model_validate(response.json())
26+
27+
def _retrieve_latest_exchange_message_spec(
28+
self, conversation_id: str, exchange_id: str, message_id: str
29+
) -> RequestSpec:
30+
return RequestSpec(
31+
method="GET",
32+
endpoint=Endpoint(
33+
f"/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}"
34+
),
35+
)

src/uipath/_uipath.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
BucketsService,
1313
ConnectionsService,
1414
ContextGroundingService,
15+
ConversationsService,
1516
DocumentsService,
1617
EntitiesService,
1718
FolderService,
@@ -147,3 +148,7 @@ def llm(self) -> UiPathLlmChatService:
147148
@property
148149
def entities(self) -> EntitiesService:
149150
return EntitiesService(self._config, self._execution_context)
151+
152+
@property
153+
def conversational(self) -> ConversationsService:
154+
return ConversationsService(self._config, self._execution_context)

src/uipath/agent/conversation/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@
5959
UiPathInlineValue,
6060
)
6161
from .conversation import (
62+
GetConversationsResponse,
6263
UiPathConversationCapabilities,
6364
UiPathConversationEndEvent,
6465
UiPathConversationStartedEvent,
6566
UiPathConversationStartEvent,
6667
)
6768
from .event import UiPathConversationEvent
6869
from .exchange import (
70+
GetExchangesResponse,
6971
UiPathConversationExchange,
7072
UiPathConversationExchangeEndEvent,
7173
UiPathConversationExchangeEvent,
@@ -94,11 +96,13 @@
9496
"UiPathConversationStartEvent",
9597
"UiPathConversationStartedEvent",
9698
"UiPathConversationEndEvent",
99+
"GetConversationsResponse",
97100
# Exchange
98101
"UiPathConversationExchangeStartEvent",
99102
"UiPathConversationExchangeEndEvent",
100103
"UiPathConversationExchangeEvent",
101104
"UiPathConversationExchange",
105+
"GetExchangesResponse",
102106
# Message
103107
"UiPathConversationMessageStartEvent",
104108
"UiPathConversationMessageEndEvent",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import Generic, List, Optional, TypeVar
2+
3+
from pydantic import BaseModel, ConfigDict, Field
4+
5+
T = TypeVar("T")
6+
7+
8+
class GetResponse(BaseModel, Generic[T]):
9+
"""Generic response for paginated GET requests."""
10+
11+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
12+
13+
data: List[T] = Field(alias="data")
14+
cursor: Optional[str] = Field(default=None, alias="cursor")

src/uipath/agent/conversation/citation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class UiPathConversationCitationSource(BaseModel):
5252

5353
# Union of Url or Media
5454
url: Optional[str] = None
55+
number: Optional[int] = None
5556
mime_type: Optional[str] = Field(None, alias="mimeType")
5657
download_url: Optional[str] = Field(None, alias="downloadUrl")
5758
page_number: Optional[str] = Field(None, alias="pageNumber")

src/uipath/agent/conversation/conversation.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from pydantic import BaseModel, ConfigDict, Field
66

7+
from ._api_responses import GetResponse
8+
79

810
class UiPathConversationCapabilities(BaseModel):
911
"""Describes the capabilities of a conversation participant."""
@@ -47,3 +49,16 @@ class UiPathConversationEndEvent(BaseModel):
4749
meta_data: Optional[Dict[str, Any]] = Field(None, alias="metaData")
4850

4951
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
52+
53+
54+
class UiPathConversationModel(BaseModel):
55+
"""Model for conversation, used get mapping jobKey to conversationId."""
56+
57+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
58+
59+
conversation_id: str = Field(alias="conversationId")
60+
job_key: str = Field(alias="jobKey")
61+
62+
63+
"""Paginated response for GET conversations."""
64+
GetConversationsResponse = GetResponse[UiPathConversationModel]

src/uipath/agent/conversation/exchange.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from pydantic import BaseModel, ConfigDict, Field
2222

23+
from ._api_responses import GetResponse
2324
from .message import UiPathConversationMessage, UiPathConversationMessageEvent
2425

2526

@@ -59,3 +60,7 @@ class UiPathConversationExchange(BaseModel):
5960
messages: List[UiPathConversationMessage]
6061

6162
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
63+
64+
65+
"""Paginated response for GET conversations."""
66+
GetExchangesResponse = GetResponse[UiPathConversationExchange]
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import pytest
2+
from pytest_httpx import HTTPXMock
3+
4+
from uipath._config import Config
5+
from uipath._execution_context import ExecutionContext
6+
from uipath._services.conversations_service import ConversationsService
7+
from uipath._utils.constants import HEADER_USER_AGENT
8+
from uipath.agent.conversation import UiPathConversationMessage
9+
10+
11+
@pytest.fixture
12+
def service(
13+
config: Config, execution_context: ExecutionContext
14+
) -> ConversationsService:
15+
return ConversationsService(config=config, execution_context=execution_context)
16+
17+
18+
class TestConversationsService:
19+
class TestRetrieveLatestExchangeMessage:
20+
@pytest.mark.anyio
21+
async def test_retrieve_latest_exchange_message(
22+
self,
23+
httpx_mock: HTTPXMock,
24+
service: ConversationsService,
25+
base_url: str,
26+
org: str,
27+
tenant: str,
28+
version: str,
29+
) -> None:
30+
"""Test retrieving a specific message from an exchange."""
31+
conversation_id = "123"
32+
exchange_id = "202cf2d1-926e-422d-8cf2-4f5735fa91fa"
33+
message_id = "08de239e-90da-4d17-b986-b7785268d8d7"
34+
35+
httpx_mock.add_response(
36+
url=f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}",
37+
status_code=200,
38+
json={
39+
"messageId": message_id,
40+
"role": "assistant",
41+
"contentParts": [],
42+
"createdAt": "2024-01-01T00:00:00Z",
43+
"updatedAt": "2024-01-01T00:00:00Z",
44+
},
45+
)
46+
47+
result = await service.retrieve_latest_exchange_message_async(
48+
conversation_id=conversation_id,
49+
exchange_id=exchange_id,
50+
message_id=message_id,
51+
)
52+
53+
assert isinstance(result, UiPathConversationMessage)
54+
assert result.message_id == message_id
55+
assert result.role == "assistant"
56+
57+
sent_request = httpx_mock.get_request()
58+
if sent_request is None:
59+
raise Exception("No request was sent")
60+
61+
assert sent_request.method == "GET"
62+
assert (
63+
sent_request.url
64+
== f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}"
65+
)
66+
67+
assert HEADER_USER_AGENT in sent_request.headers
68+
assert (
69+
sent_request.headers[HEADER_USER_AGENT]
70+
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ConversationsService.retrieve_latest_exchange_message_async/{version}"
71+
)
72+
73+
@pytest.mark.anyio
74+
async def test_retrieve_latest_exchange_message_with_content_parts(
75+
self,
76+
httpx_mock: HTTPXMock,
77+
service: ConversationsService,
78+
base_url: str,
79+
org: str,
80+
tenant: str,
81+
) -> None:
82+
"""Test retrieving a message with content parts."""
83+
conversation_id = "123"
84+
exchange_id = "202cf2d1-926e-422d-8cf2-4f5735fa91fa"
85+
message_id = "08de239e-90da-4d17-b986-b7785268d8d7"
86+
87+
httpx_mock.add_response(
88+
url=f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}",
89+
status_code=200,
90+
json={
91+
"messageId": message_id,
92+
"role": "user",
93+
"contentParts": [
94+
{
95+
"contentPartId": "cp-1",
96+
"mimeType": "text/plain",
97+
"data": {"inline": "Hello, world!"},
98+
}
99+
],
100+
"createdAt": "2024-01-01T00:00:00Z",
101+
"updatedAt": "2024-01-01T00:00:00Z",
102+
},
103+
)
104+
105+
result = await service.retrieve_latest_exchange_message_async(
106+
conversation_id=conversation_id,
107+
exchange_id=exchange_id,
108+
message_id=message_id,
109+
)
110+
111+
assert isinstance(result, UiPathConversationMessage)
112+
assert result.message_id == message_id
113+
assert result.role == "user"
114+
assert result.content_parts is not None
115+
assert len(result.content_parts) == 1
116+
assert result.content_parts[0].content_part_id == "cp-1"
117+
assert result.content_parts[0].mime_type == "text/plain"
118+
119+
@pytest.mark.anyio
120+
async def test_retrieve_latest_exchange_message_with_tool_calls(
121+
self,
122+
httpx_mock: HTTPXMock,
123+
service: ConversationsService,
124+
base_url: str,
125+
org: str,
126+
tenant: str,
127+
) -> None:
128+
"""Test retrieving a message with tool calls."""
129+
conversation_id = "123"
130+
exchange_id = "202cf2d1-926e-422d-8cf2-4f5735fa91fa"
131+
message_id = "08de239e-90da-4d17-b986-b7785268d8d7"
132+
133+
httpx_mock.add_response(
134+
url=f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}",
135+
status_code=200,
136+
json={
137+
"messageId": message_id,
138+
"role": "assistant",
139+
"contentParts": [],
140+
"toolCalls": [
141+
{
142+
"toolCallId": "tc-1",
143+
"name": "get_weather",
144+
"arguments": {"inline": '{"city": "San Francisco"}'},
145+
}
146+
],
147+
"createdAt": "2024-01-01T00:00:00Z",
148+
"updatedAt": "2024-01-01T00:00:00Z",
149+
},
150+
)
151+
152+
result = await service.retrieve_latest_exchange_message_async(
153+
conversation_id=conversation_id,
154+
exchange_id=exchange_id,
155+
message_id=message_id,
156+
)
157+
158+
assert isinstance(result, UiPathConversationMessage)
159+
assert result.message_id == message_id
160+
assert result.role == "assistant"
161+
assert result.tool_calls is not None
162+
assert len(result.tool_calls) == 1
163+
assert result.tool_calls[0].tool_call_id == "tc-1"
164+
assert result.tool_calls[0].name == "get_weather"

0 commit comments

Comments
 (0)