Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/uipath/_services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .buckets_service import BucketsService
from .connections_service import ConnectionsService
from .context_grounding_service import ContextGroundingService
from .conversations_service import ConversationsService
from .documents_service import DocumentsService
from .entities_service import EntitiesService
from .external_application_service import ExternalApplicationService
Expand All @@ -31,4 +32,5 @@
"FolderService",
"EntitiesService",
"ExternalApplicationService",
"ConversationsService",
]
35 changes: 35 additions & 0 deletions src/uipath/_services/conversations_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from .._config import Config
from .._execution_context import ExecutionContext
from .._utils import Endpoint, RequestSpec
from ..agent.conversation import UiPathConversationMessage
from ..tracing import traced
from ._base_service import BaseService


class ConversationsService(BaseService):
def __init__(self, config: Config, execution_context: ExecutionContext) -> None:
super().__init__(config=config, execution_context=execution_context)

@traced(name="retrieve_latest_exchange_message", run_type="uipath")
async def retrieve_latest_exchange_message_async(
self, conversation_id: str, exchange_id: str, message_id: str
) -> UiPathConversationMessage:
retrieve_message_spec = self._retrieve_latest_exchange_message_spec(
conversation_id, exchange_id, message_id
)

response = await self.request_async(
retrieve_message_spec.method, retrieve_message_spec.endpoint
)

return UiPathConversationMessage.model_validate(response.json())

def _retrieve_latest_exchange_message_spec(
self, conversation_id: str, exchange_id: str, message_id: str
) -> RequestSpec:
return RequestSpec(
method="GET",
endpoint=Endpoint(
f"/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}"
),
)
5 changes: 5 additions & 0 deletions src/uipath/_uipath.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BucketsService,
ConnectionsService,
ContextGroundingService,
ConversationsService,
DocumentsService,
EntitiesService,
FolderService,
Expand Down Expand Up @@ -147,3 +148,7 @@ def llm(self) -> UiPathLlmChatService:
@property
def entities(self) -> EntitiesService:
return EntitiesService(self._config, self._execution_context)

@property
def conversational(self) -> ConversationsService:
return ConversationsService(self._config, self._execution_context)
4 changes: 4 additions & 0 deletions src/uipath/agent/conversation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@
UiPathInlineValue,
)
from .conversation import (
GetConversationsResponse,
UiPathConversationCapabilities,
UiPathConversationEndEvent,
UiPathConversationStartedEvent,
UiPathConversationStartEvent,
)
from .event import UiPathConversationEvent
from .exchange import (
GetExchangesResponse,
UiPathConversationExchange,
UiPathConversationExchangeEndEvent,
UiPathConversationExchangeEvent,
Expand Down Expand Up @@ -94,11 +96,13 @@
"UiPathConversationStartEvent",
"UiPathConversationStartedEvent",
"UiPathConversationEndEvent",
"GetConversationsResponse",
# Exchange
"UiPathConversationExchangeStartEvent",
"UiPathConversationExchangeEndEvent",
"UiPathConversationExchangeEvent",
"UiPathConversationExchange",
"GetExchangesResponse",
# Message
"UiPathConversationMessageStartEvent",
"UiPathConversationMessageEndEvent",
Expand Down
14 changes: 14 additions & 0 deletions src/uipath/agent/conversation/_api_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from typing import Generic, List, Optional, TypeVar

from pydantic import BaseModel, ConfigDict, Field

T = TypeVar("T")


class GetResponse(BaseModel, Generic[T]):
"""Generic response for paginated GET requests."""

model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)

data: List[T] = Field(alias="data")
cursor: Optional[str] = Field(default=None, alias="cursor")
1 change: 1 addition & 0 deletions src/uipath/agent/conversation/citation.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class UiPathConversationCitationSource(BaseModel):

# Union of Url or Media
url: Optional[str] = None
number: Optional[int] = None
mime_type: Optional[str] = Field(None, alias="mimeType")
download_url: Optional[str] = Field(None, alias="downloadUrl")
page_number: Optional[str] = Field(None, alias="pageNumber")
Expand Down
15 changes: 15 additions & 0 deletions src/uipath/agent/conversation/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from pydantic import BaseModel, ConfigDict, Field

from ._api_responses import GetResponse


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

model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)


class UiPathConversationModel(BaseModel):
"""Model for conversation, used get mapping jobKey to conversationId."""

model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)

conversation_id: str = Field(alias="conversationId")
job_key: str = Field(alias="jobKey")


"""Paginated response for GET conversations."""
GetConversationsResponse = GetResponse[UiPathConversationModel]
5 changes: 5 additions & 0 deletions src/uipath/agent/conversation/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from pydantic import BaseModel, ConfigDict, Field

from ._api_responses import GetResponse
from .message import UiPathConversationMessage, UiPathConversationMessageEvent


Expand Down Expand Up @@ -59,3 +60,7 @@ class UiPathConversationExchange(BaseModel):
messages: List[UiPathConversationMessage]

model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)


"""Paginated response for GET conversations."""
GetExchangesResponse = GetResponse[UiPathConversationExchange]
164 changes: 164 additions & 0 deletions tests/sdk/services/test_conversations_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import pytest
from pytest_httpx import HTTPXMock

from uipath._config import Config
from uipath._execution_context import ExecutionContext
from uipath._services.conversations_service import ConversationsService
from uipath._utils.constants import HEADER_USER_AGENT
from uipath.agent.conversation import UiPathConversationMessage


@pytest.fixture
def service(
config: Config, execution_context: ExecutionContext
) -> ConversationsService:
return ConversationsService(config=config, execution_context=execution_context)


class TestConversationsService:
class TestRetrieveLatestExchangeMessage:
@pytest.mark.anyio
async def test_retrieve_latest_exchange_message(
self,
httpx_mock: HTTPXMock,
service: ConversationsService,
base_url: str,
org: str,
tenant: str,
version: str,
) -> None:
"""Test retrieving a specific message from an exchange."""
conversation_id = "123"
exchange_id = "202cf2d1-926e-422d-8cf2-4f5735fa91fa"
message_id = "08de239e-90da-4d17-b986-b7785268d8d7"

httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}",
status_code=200,
json={
"messageId": message_id,
"role": "assistant",
"contentParts": [],
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
},
)

result = await service.retrieve_latest_exchange_message_async(
conversation_id=conversation_id,
exchange_id=exchange_id,
message_id=message_id,
)

assert isinstance(result, UiPathConversationMessage)
assert result.message_id == message_id
assert result.role == "assistant"

sent_request = httpx_mock.get_request()
if sent_request is None:
raise Exception("No request was sent")

assert sent_request.method == "GET"
assert (
sent_request.url
== f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}"
)

assert HEADER_USER_AGENT in sent_request.headers
assert (
sent_request.headers[HEADER_USER_AGENT]
== f"UiPath.Python.Sdk/UiPath.Python.Sdk.Activities.ConversationsService.retrieve_latest_exchange_message_async/{version}"
)

@pytest.mark.anyio
async def test_retrieve_latest_exchange_message_with_content_parts(
self,
httpx_mock: HTTPXMock,
service: ConversationsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""Test retrieving a message with content parts."""
conversation_id = "123"
exchange_id = "202cf2d1-926e-422d-8cf2-4f5735fa91fa"
message_id = "08de239e-90da-4d17-b986-b7785268d8d7"

httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}",
status_code=200,
json={
"messageId": message_id,
"role": "user",
"contentParts": [
{
"contentPartId": "cp-1",
"mimeType": "text/plain",
"data": {"inline": "Hello, world!"},
}
],
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
},
)

result = await service.retrieve_latest_exchange_message_async(
conversation_id=conversation_id,
exchange_id=exchange_id,
message_id=message_id,
)

assert isinstance(result, UiPathConversationMessage)
assert result.message_id == message_id
assert result.role == "user"
assert result.content_parts is not None
assert len(result.content_parts) == 1
assert result.content_parts[0].content_part_id == "cp-1"
assert result.content_parts[0].mime_type == "text/plain"

@pytest.mark.anyio
async def test_retrieve_latest_exchange_message_with_tool_calls(
self,
httpx_mock: HTTPXMock,
service: ConversationsService,
base_url: str,
org: str,
tenant: str,
) -> None:
"""Test retrieving a message with tool calls."""
conversation_id = "123"
exchange_id = "202cf2d1-926e-422d-8cf2-4f5735fa91fa"
message_id = "08de239e-90da-4d17-b986-b7785268d8d7"

httpx_mock.add_response(
url=f"{base_url}{org}{tenant}/autopilotforeveryone_/api/v1/conversation/{conversation_id}/exchange/{exchange_id}/message/{message_id}",
status_code=200,
json={
"messageId": message_id,
"role": "assistant",
"contentParts": [],
"toolCalls": [
{
"toolCallId": "tc-1",
"name": "get_weather",
"arguments": {"inline": '{"city": "San Francisco"}'},
}
],
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z",
},
)

result = await service.retrieve_latest_exchange_message_async(
conversation_id=conversation_id,
exchange_id=exchange_id,
message_id=message_id,
)

assert isinstance(result, UiPathConversationMessage)
assert result.message_id == message_id
assert result.role == "assistant"
assert result.tool_calls is not None
assert len(result.tool_calls) == 1
assert result.tool_calls[0].tool_call_id == "tc-1"
assert result.tool_calls[0].name == "get_weather"