Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 packages/uipath-core/src/uipath/core/chat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
UiPathSessionStartEvent,
)
from .tool import (
UiPathConversationExecutingToolCallEvent,
Comment thread
norman-le marked this conversation as resolved.
UiPathConversationToolCall,
UiPathConversationToolCallConfirmation,
UiPathConversationToolCallConfirmationData,
Expand Down Expand Up @@ -171,6 +172,7 @@
"UiPathConversationCitationData",
"UiPathConversationCitation",
# Tool
"UiPathConversationExecutingToolCallEvent",
"UiPathConversationToolCallStartEvent",
"UiPathConversationToolCallEndEvent",
"UiPathConversationToolCallConfirmation",
Expand Down
13 changes: 13 additions & 0 deletions packages/uipath-core/src/uipath/core/chat/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,24 @@
)


class UiPathClientSideToolDeclaration(BaseModel):
"""A client-side tool declaration from the SDK client."""

name: str
input_schema: dict[str, Any] | None = Field(None, alias="inputSchema")
output_schema: dict[str, Any] | None = Field(None, alias="outputSchema")

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


class UiPathConversationExchangeStartEvent(BaseModel):
"""Signals the start of an exchange of messages within a conversation."""

conversation_sequence: int | None = Field(None, alias="conversationSequence")
metadata: dict[str, Any] | None = Field(None, alias="metaData")
client_side_tools: list[UiPathClientSideToolDeclaration] | None = Field(
None, alias="clientSideTools"
)
timestamp: str | None = None

model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
Expand Down
17 changes: 17 additions & 0 deletions packages/uipath-core/src/uipath/core/chat/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class UiPathConversationToolCallStartEvent(BaseModel):
metadata: dict[str, Any] | None = Field(None, alias="metaData")
require_confirmation: bool | None = Field(None, alias="requireConfirmation")
input_schema: Any | None = Field(None, alias="inputSchema")
is_client_side_tool: bool | None = Field(None, alias="isClientSideTool")
output_schema: Any | None = Field(None, alias="outputSchema")

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

Expand All @@ -43,6 +45,18 @@ class UiPathConversationToolCallEndEvent(BaseModel):
model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)


class UiPathConversationExecutingToolCallEvent(BaseModel):
"""Signals the client that the tool is about to be executed.
Emitted in all paths. For client-side tools, the client should begin
executing its handler upon receiving this event."""

tool_name: str = Field(..., alias="toolName")
timestamp: str | None = None
input: dict[str, Any] | None = None

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


class UiPathConversationToolCallConfirmationEvent(BaseModel):
"""Signals a tool call confirmation (approve/reject) from the client."""

Expand Down Expand Up @@ -82,6 +96,9 @@ class UiPathConversationToolCallEvent(BaseModel):
confirm: UiPathConversationToolCallConfirmationEvent | None = Field(
None, alias="confirmToolCall"
)
executing: UiPathConversationExecutingToolCallEvent | None = Field(
None, alias="executingToolCall"
)
meta_event: dict[str, Any] | None = Field(None, alias="metaEvent")
error: UiPathConversationErrorEvent | None = Field(None, alias="toolCallError")

Expand Down
70 changes: 47 additions & 23 deletions packages/uipath/src/uipath/_cli/_chat/_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
UiPathConversationEvent,
UiPathConversationExchangeEndEvent,
UiPathConversationExchangeEvent,
UiPathConversationExecutingToolCallEvent,
UiPathConversationMessageEvent,
UiPathConversationToolCallConfirmationEvent,
UiPathConversationToolCallEndEvent,
UiPathConversationToolCallEvent,
)
from uipath.core.triggers import UiPathResumeTrigger
from uipath.runtime.chat import UiPathChatProtocol
Expand Down Expand Up @@ -124,7 +127,9 @@ def __init__(

self._tool_confirmation_event = asyncio.Event()
self._tool_confirmation_value: (
UiPathConversationToolCallConfirmationEvent | None
UiPathConversationToolCallConfirmationEvent
| UiPathConversationToolCallEndEvent
| None
Comment thread
norman-le marked this conversation as resolved.
Outdated
) = None
self._current_message_id: str | None = None

Expand Down Expand Up @@ -360,23 +365,42 @@ async def emit_exchange_error_event(self, error: Exception) -> None:
raise RuntimeError(f"Failed to send exchange error event: {e}") from e

async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger):
"""No-op.

Tool confirmation — the only interrupt pattern CAS uses today — is
handled end-to-end via ``startToolCall`` with ``requireConfirmation:
true`` paired with ``wait_for_resume()``. This is deliberately
simpler than the old interrupt-based flow: CAS needs
``requireConfirmation`` on the tool call event itself to render the
confirmation UI, so a parallel ``startInterrupt`` event would be
redundant.

The only hypothetical reason to put work here is a generic,
non-tool-call agent interrupt (e.g. a coded agent calling
``interrupt("do you want to continue?")``). Nothing uses that today
and it's not a near-term requirement — the method is kept for
generic flexibility.
"""Emit an executingToolCall event if the trigger is marked with is_execution_phase.

Called by the runtime loop for every durable interrupt. Emits executingToolCall
for triggers that signal execution is about to begin, The
Comment thread
norman-le marked this conversation as resolved.
Outdated
is_execution_phase marker ensures it fires exactly once per tool call.
"""
return None
request = (
resume_trigger.api_resume.request if resume_trigger.api_resume else None
)
if not request or not isinstance(request, dict):
return

if not request.get("is_execution_phase"):
return

tool_call_id = request.get("tool_call_id")
tool_name = request.get("tool_name")
tool_input = request.get("input")

if not tool_call_id or not tool_name:
return

if not self._current_message_id:
return

executing_event = UiPathConversationMessageEvent(
message_id=self._current_message_id,
tool_call=UiPathConversationToolCallEvent(
tool_call_id=tool_call_id,
executing=UiPathConversationExecutingToolCallEvent(
tool_name=tool_name,
input=tool_input,
),
),
)
await self.emit_message_event(executing_event)

async def wait_for_resume(self) -> dict[str, Any]:
"""Wait for a confirmToolCall event to be received."""
Expand Down Expand Up @@ -424,13 +448,13 @@ async def _handle_conversation_event(
parsed_event.exchange
and parsed_event.exchange.message
and (tool_call := parsed_event.exchange.message.tool_call)
and (confirm := tool_call.confirm)
):
logger.info(
f"Received confirmToolCall for tool_call_id: {tool_call.tool_call_id}, approved: {confirm.approved}"
)
self._tool_confirmation_value = confirm
self._tool_confirmation_event.set()
if confirm := tool_call.confirm:
self._tool_confirmation_value = confirm
self._tool_confirmation_event.set()
elif end := tool_call.end:
self._tool_confirmation_value = end
self._tool_confirmation_event.set()
Comment thread
norman-le marked this conversation as resolved.
Outdated
except Exception as e:
logger.warning(f"Error parsing conversation event: {e}")

Expand Down
12 changes: 12 additions & 0 deletions packages/uipath/src/uipath/agent/models/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ class AgentToolType(str, CaseInsensitiveEnum):
INTEGRATION = "Integration"
INTERNAL = "Internal"
IXP = "Ixp"
CLIENT_SIDE = "ClientSide"
UNKNOWN = "Unknown" # fallback branch discriminator


Expand Down Expand Up @@ -895,6 +896,15 @@ class AgentInternalToolResourceConfig(BaseAgentToolResourceConfig):
)


class AgentClientSideToolResourceConfig(BaseAgentToolResourceConfig):
"""Resource config for client-side tools executed by the client SDK."""

type: Literal[AgentToolType.CLIENT_SIDE] = AgentToolType.CLIENT_SIDE
properties: BaseResourceProperties = Field(default_factory=BaseResourceProperties)
output_schema: Optional[Dict[str, Any]] = Field(None, alias="outputSchema")
arguments: Optional[Dict[str, Any]] = Field(default_factory=dict)


class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
"""Fallback for unknown tool types (parent normalizer sets type='Unknown')."""

Expand All @@ -908,6 +918,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
AgentIntegrationToolResourceConfig,
AgentInternalToolResourceConfig,
AgentIxpExtractionResourceConfig,
AgentClientSideToolResourceConfig,
AgentUnknownToolResourceConfig, # when parent sets type="Unknown"
],
Field(discriminator="type"),
Expand Down Expand Up @@ -1290,6 +1301,7 @@ def _normalize_resources(v: Dict[str, Any]) -> None:
"integration": "Integration",
"internal": "Internal",
"ixp": "Ixp",
"clientside": "ClientSide",
"unknown": "Unknown",
}
CONTEXT_MODE_MAP = {
Expand Down
Loading
Loading