Skip to content

Commit f234583

Browse files
authored
feat: add client side tools to bridge and new cas events [JAR-9629] (#1638)
1 parent 1b81042 commit f234583

12 files changed

Lines changed: 567 additions & 37 deletions

File tree

packages/uipath-core/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-core"
3-
version = "0.5.16"
3+
version = "0.5.17"
44
description = "UiPath Core abstractions"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath-core/src/uipath/core/chat/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
)
7272
from .event import UiPathConversationEvent, UiPathConversationLabelUpdatedEvent
7373
from .exchange import (
74+
UiPathClientSideToolDeclaration,
7475
UiPathConversationExchange,
7576
UiPathConversationExchangeData,
7677
UiPathConversationExchangeEndEvent,
@@ -107,6 +108,7 @@
107108
UiPathSessionStartEvent,
108109
)
109110
from .tool import (
111+
UiPathConversationExecutingToolCallEvent,
110112
UiPathConversationToolCall,
111113
UiPathConversationToolCallConfirmation,
112114
UiPathConversationToolCallConfirmationData,
@@ -138,6 +140,7 @@
138140
"UiPathSessionEndingEvent",
139141
"UiPathSessionEndEvent",
140142
# Exchange
143+
"UiPathClientSideToolDeclaration",
141144
"UiPathConversationExchangeStartEvent",
142145
"UiPathConversationExchangeEndEvent",
143146
"UiPathConversationExchangeEvent",
@@ -171,6 +174,7 @@
171174
"UiPathConversationCitationData",
172175
"UiPathConversationCitation",
173176
# Tool
177+
"UiPathConversationExecutingToolCallEvent",
174178
"UiPathConversationToolCallStartEvent",
175179
"UiPathConversationToolCallEndEvent",
176180
"UiPathConversationToolCallConfirmation",

packages/uipath-core/src/uipath/core/chat/exchange.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,24 @@
2828
)
2929

3030

31+
class UiPathClientSideToolDeclaration(BaseModel):
32+
"""A client-side tool declaration from the SDK client."""
33+
34+
name: str
35+
input_schema: dict[str, Any] | None = Field(None, alias="inputSchema")
36+
output_schema: dict[str, Any] | None = Field(None, alias="outputSchema")
37+
38+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
39+
40+
3141
class UiPathConversationExchangeStartEvent(BaseModel):
3242
"""Signals the start of an exchange of messages within a conversation."""
3343

3444
conversation_sequence: int | None = Field(None, alias="conversationSequence")
3545
metadata: dict[str, Any] | None = Field(None, alias="metaData")
46+
client_side_tools: list[UiPathClientSideToolDeclaration] | None = Field(
47+
None, alias="clientSideTools"
48+
)
3649
timestamp: str | None = None
3750

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

packages/uipath-core/src/uipath/core/chat/tool.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class UiPathConversationToolCallStartEvent(BaseModel):
2727
metadata: dict[str, Any] | None = Field(None, alias="metaData")
2828
require_confirmation: bool | None = Field(None, alias="requireConfirmation")
2929
input_schema: Any | None = Field(None, alias="inputSchema")
30+
is_client_side_tool: bool | None = Field(None, alias="isClientSideTool")
31+
output_schema: Any | None = Field(None, alias="outputSchema")
3032

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

@@ -43,6 +45,19 @@ class UiPathConversationToolCallEndEvent(BaseModel):
4345
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
4446

4547

48+
class UiPathConversationExecutingToolCallEvent(BaseModel):
49+
"""Signals the client that the tool is about to be executed.
50+
51+
Emitted in all scenarios. For client-side tools, the client should begin
52+
executing its handler upon receiving this event.
53+
"""
54+
55+
timestamp: str | None = None
56+
input: dict[str, Any] | None = None
57+
58+
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
59+
60+
4661
class UiPathConversationToolCallConfirmationEvent(BaseModel):
4762
"""Signals a tool call confirmation (approve/reject) from the client."""
4863

@@ -82,6 +97,9 @@ class UiPathConversationToolCallEvent(BaseModel):
8297
confirm: UiPathConversationToolCallConfirmationEvent | None = Field(
8398
None, alias="confirmToolCall"
8499
)
100+
executing: UiPathConversationExecutingToolCallEvent | None = Field(
101+
None, alias="executingToolCall"
102+
)
85103
meta_event: dict[str, Any] | None = Field(None, alias="metaEvent")
86104
error: UiPathConversationErrorEvent | None = Field(None, alias="toolCallError")
87105

packages/uipath-core/uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath-platform/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uipath/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"
3-
version = "2.10.72"
3+
version = "2.10.73"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/_cli/_chat/_bridge.py

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
UiPathConversationEvent,
1414
UiPathConversationExchangeEndEvent,
1515
UiPathConversationExchangeEvent,
16+
UiPathConversationExecutingToolCallEvent,
1617
UiPathConversationMessageEvent,
1718
UiPathConversationToolCallConfirmationEvent,
19+
UiPathConversationToolCallEndEvent,
20+
UiPathConversationToolCallEvent,
1821
)
1922
from uipath.core.triggers import UiPathResumeTrigger
2023
from uipath.runtime.chat import UiPathChatProtocol
@@ -122,9 +125,11 @@ def __init__(
122125
self._client: Any | None = None
123126
self._connected_event = asyncio.Event()
124127

125-
self._tool_confirmation_event = asyncio.Event()
126-
self._tool_confirmation_value: (
127-
UiPathConversationToolCallConfirmationEvent | None
128+
self._tool_resume_event = asyncio.Event()
129+
self._tool_resume_value: (
130+
UiPathConversationToolCallConfirmationEvent
131+
| UiPathConversationToolCallEndEvent
132+
| None
128133
) = None
129134
self._current_message_id: str | None = None
130135

@@ -362,33 +367,52 @@ async def emit_exchange_error_event(self, error: Exception) -> None:
362367
async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger):
363368
"""No-op.
364369
365-
Tool confirmation — the only interrupt pattern CAS uses today — is
366-
handled end-to-end via ``startToolCall`` with ``requireConfirmation:
367-
true`` paired with ``wait_for_resume()``. This is deliberately
368-
simpler than the old interrupt-based flow: CAS needs
369-
``requireConfirmation`` on the tool call event itself to render the
370-
confirmation UI, so a parallel ``startInterrupt`` event would be
371-
redundant.
372-
373-
The only hypothetical reason to put work here is a generic,
374-
non-tool-call agent interrupt (e.g. a coded agent calling
375-
``interrupt("do you want to continue?")``). Nothing uses that today
376-
and it's not a near-term requirement — the method is kept for
377-
generic flexibility.
370+
Tool confirmation is handled end-to-end via ``startToolCall`` with
371+
``requireConfirmation: true`` paired with ``wait_for_resume()``.
372+
executingToolCall is emitted by the MessageMapper (non-confirmed
373+
tools) and the runtime loop post-confirmation (confirmed tools).
378374
"""
379375
return None
380376

377+
async def emit_executing_tool_call_event(
378+
self,
379+
tool_call_id: str,
380+
tool_input: dict[str, Any] | None = None,
381+
) -> None:
382+
"""Emit an executingToolCall event.
383+
384+
Called by the runtime loop after a tool-call confirmation resumes
385+
to signal that the tool is about to execute with the final input.
386+
"""
387+
if not self._current_message_id:
388+
return
389+
390+
executing_event = UiPathConversationMessageEvent(
391+
message_id=self._current_message_id,
392+
tool_call=UiPathConversationToolCallEvent(
393+
tool_call_id=tool_call_id,
394+
executing=UiPathConversationExecutingToolCallEvent(
395+
input=tool_input,
396+
),
397+
),
398+
)
399+
await self.emit_message_event(executing_event)
400+
381401
async def wait_for_resume(self) -> dict[str, Any]:
382-
"""Wait for a confirmToolCall event to be received."""
383-
self._tool_confirmation_event.clear()
384-
self._tool_confirmation_value = None
402+
"""Wait for a tool resume event (confirmToolCall or endToolCall) to be received."""
403+
if self._tool_resume_value is None:
404+
self._tool_resume_event.clear()
405+
await self._tool_resume_event.wait()
385406

386-
await self._tool_confirmation_event.wait()
407+
value = self._tool_resume_value
408+
self._tool_resume_value = None
409+
self._tool_resume_event.clear()
387410

388-
if self._tool_confirmation_value:
389-
return self._tool_confirmation_value.model_dump(
390-
mode="python", by_alias=False
391-
)
411+
"""For the case where there's no tool confirmation and the client side tool sends endToolCall back before wait_for_resume is called.
412+
Unlikely in practice, but possible in theory, since executingToolCall is emitted during the streaming.
413+
"""
414+
if value:
415+
return value.model_dump(mode="python", by_alias=False)
392416
return {}
393417

394418
@property
@@ -424,13 +448,13 @@ async def _handle_conversation_event(
424448
parsed_event.exchange
425449
and parsed_event.exchange.message
426450
and (tool_call := parsed_event.exchange.message.tool_call)
427-
and (confirm := tool_call.confirm)
428451
):
429-
logger.info(
430-
f"Received confirmToolCall for tool_call_id: {tool_call.tool_call_id}, approved: {confirm.approved}"
431-
)
432-
self._tool_confirmation_value = confirm
433-
self._tool_confirmation_event.set()
452+
if confirm := tool_call.confirm:
453+
self._tool_resume_value = confirm
454+
self._tool_resume_event.set()
455+
elif end := tool_call.end:
456+
self._tool_resume_value = end
457+
self._tool_resume_event.set()
434458
except Exception as e:
435459
logger.warning(f"Error parsing conversation event: {e}")
436460

packages/uipath/src/uipath/agent/models/agent.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class AgentToolType(str, CaseInsensitiveEnum):
117117
INTEGRATION = "Integration"
118118
INTERNAL = "Internal"
119119
IXP = "Ixp"
120+
CLIENT_SIDE = "ClientSide"
120121
UNKNOWN = "Unknown" # fallback branch discriminator
121122

122123

@@ -945,6 +946,15 @@ class AgentInternalToolResourceConfig(BaseAgentToolResourceConfig):
945946
)
946947

947948

949+
class AgentClientSideToolResourceConfig(BaseAgentToolResourceConfig):
950+
"""Resource config for client-side tools executed by the client SDK."""
951+
952+
type: Literal[AgentToolType.CLIENT_SIDE] = AgentToolType.CLIENT_SIDE
953+
properties: BaseResourceProperties = Field(default_factory=BaseResourceProperties)
954+
output_schema: Optional[Dict[str, Any]] = Field(None, alias="outputSchema")
955+
arguments: Optional[Dict[str, Any]] = Field(default_factory=dict)
956+
957+
948958
class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
949959
"""Fallback for unknown tool types (parent normalizer sets type='Unknown')."""
950960

@@ -958,6 +968,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
958968
AgentIntegrationToolResourceConfig,
959969
AgentInternalToolResourceConfig,
960970
AgentIxpExtractionResourceConfig,
971+
AgentClientSideToolResourceConfig,
961972
AgentUnknownToolResourceConfig, # when parent sets type="Unknown"
962973
],
963974
Field(discriminator="type"),
@@ -1342,6 +1353,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None:
13421353
"integration": "Integration",
13431354
"internal": "Internal",
13441355
"ixp": "Ixp",
1356+
"clientside": "ClientSide",
13451357
"unknown": "Unknown",
13461358
}
13471359
CONTEXT_MODE_MAP = {

0 commit comments

Comments
 (0)