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 packages/uipath-core/src/uipath/core/chat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
UiPathSessionStartEvent,
)
from .tool import (
UiPathConversationExecutingToolCallEvent,
UiPathConversationToolCall,
UiPathConversationToolCallConfirmation,
UiPathConversationToolCallConfirmationData,
Expand Down Expand Up @@ -157,6 +158,7 @@
"UiPathConversationCitationData",
"UiPathConversationCitation",
# Tool
"UiPathConversationExecutingToolCallEvent",
"UiPathConversationToolCallStartEvent",
"UiPathConversationToolCallEndEvent",
"UiPathConversationToolCallConfirmation",
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
61 changes: 54 additions & 7 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
) = None
Comment on lines 128 to 133
self._current_message_id: str | None = None

Expand Down Expand Up @@ -378,6 +383,48 @@ async def emit_interrupt_event(self, resume_trigger: UiPathResumeTrigger):
"""
return None

async def emit_executing_tool_call_event(
self, resume_trigger: UiPathResumeTrigger
) -> None:
"""Emit an executingToolCall event for client-side tool execution.

Only emits for triggers marked with is_execution_phase=True.
This fires exactly once per client-side tool call — for Path 3 (no confirm)
and for Path 4 (after confirmation, on the execution interrupt).
Confirmation-only interrupts (Paths 2/4 first interrupt) are skipped.
"""
Comment on lines +386 to +395

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:
logger.info(
f"emit_executing_tool_call_event: missing tool_call_id or tool_name, skipping. tool_call_id={tool_call_id}, tool_name={tool_name}"
)
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,
),
Comment on lines +406 to +423
),
)
await self.emit_message_event(executing_event)

async def wait_for_resume(self) -> dict[str, Any]:
"""Wait for a confirmToolCall event to be received."""
self._tool_confirmation_event.clear()
Expand Down Expand Up @@ -424,13 +471,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()
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 @@ -889,6 +890,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)

Comment on lines +893 to +900

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

Expand All @@ -902,6 +912,7 @@ class AgentUnknownToolResourceConfig(BaseAgentToolResourceConfig):
AgentIntegrationToolResourceConfig,
AgentInternalToolResourceConfig,
AgentIxpExtractionResourceConfig,
AgentClientSideToolResourceConfig,
AgentUnknownToolResourceConfig, # when parent sets type="Unknown"
],
Field(discriminator="type"),
Expand Down Expand Up @@ -1276,6 +1287,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