Skip to content
Closed
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
83 changes: 69 additions & 14 deletions demo/mock_support_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
UiPathConversationContentPartEvent,
UiPathConversationContentPartStartEvent,
)
from uipath.core.chat.interrupt import (
UiPathConversationToolCallConfirmationValue,
)
from uipath.core.chat.message import (
UiPathConversationMessageEndEvent,
UiPathConversationMessageStartEvent,
Expand Down Expand Up @@ -197,6 +194,8 @@ def __init__(self, entrypoint: str = ENTRYPOINT_SUPPORT_CHAT) -> None:
self.tracer = trace.get_tracer("uipath.dev.mock.support-chat")
self._suspended_turn: dict[str, Any] | None = None
self._suspended_message_id: str | None = None
self._suspended_interrupt_id: str | None = None
self._suspended_call_id: str | None = None

async def get_schema(self) -> UiPathRuntimeSchema:
"""Get the schema for the support chat runtime."""
Expand Down Expand Up @@ -505,8 +504,49 @@ async def stream(
# --- Resume after tool approval ---
if is_resuming and self._suspended_turn is not None:
turn = self._suspended_turn
message_id = self._suspended_message_id or ""
interrupt_id = self._suspended_interrupt_id or ""
call_id = self._suspended_call_id or ""
self._suspended_turn = None
self._suspended_message_id = None
self._suspended_interrupt_id = None
self._suspended_call_id = None

# ``current_input`` on resume is ``{interrupt_id: {approved, input}}``
# — extract the user's decision.
confirmation = (
input.get(interrupt_id, {}) if isinstance(input, dict) else {}
)
approved = bool(confirmation.get("approved", False))
approval_tool = turn["approval_tool"]

yield UiPathRuntimeMessageEvent(
payload=UiPathConversationMessageEvent(
message_id=message_id,
tool_call=UiPathConversationToolCallEvent(
tool_call_id=call_id,
end=UiPathConversationToolCallEndEvent(
timestamp=datetime.now().isoformat(),
output=approval_tool["output"]
if approved
else "Cancelled by user.",
cancelled=not approved,
),
),
),
)

if not approved:
# User rejected — skip phase 2 and yield a SUCCESSFUL result so
# UiPathChatRuntime exits its outer loop. Returning bare here
# would leave ``execution_completed = False`` and trigger
# another ``delegate.stream`` iteration, advancing to the next
# turn.
yield UiPathRuntimeResult(
output={"reply": "Cancelled by user."},
status=UiPathRuntimeStatus.SUCCESSFUL,
)
return

resume_span = self.tracer.start_span(
"support_chat.resume",
Expand Down Expand Up @@ -639,35 +679,50 @@ async def stream(
await asyncio.sleep(0.5)
yield self._node_state("tools", C)

# --- Check if this turn needs tool approval ---
# --- Approval flow: emit startToolCall(requireConfirmation=true)
# so the UI renders the inline approve/reject panel, then suspend
# so UiPathChatRuntime awaits ``bridge.wait_for_resume()``. The
# bridge unblocks when the user clicks approve/reject and the
# confirmation flows through ``run_service.confirm_tool_call``.
if needs_approval:
approval_tool = turn["approval_tool"]
approval_call_id = f"call_{uuid4().hex[:12]}"
interrupt_id = str(uuid4())
tool_call_id = f"call_{uuid4().hex[:12]}"

# Store state for resume
yield UiPathRuntimeMessageEvent(
payload=UiPathConversationMessageEvent(
message_id=message_id,
tool_call=UiPathConversationToolCallEvent(
tool_call_id=approval_call_id,
start=UiPathConversationToolCallStartEvent(
tool_name=approval_tool["name"],
timestamp=datetime.now().isoformat(),
input=approval_tool["input"],
require_confirmation=True,
input_schema=approval_tool["input_schema"],
),
),
),
)

self._suspended_turn = turn
self._suspended_message_id = message_id
self._suspended_interrupt_id = interrupt_id
self._suspended_call_id = approval_call_id

yield UiPathRuntimeResult(
status=UiPathRuntimeStatus.SUSPENDED,
output={"paused_for_approval": approval_tool["name"]},
output={"awaiting_confirmation": approval_tool["name"]},
triggers=[
UiPathResumeTrigger(
interrupt_id=interrupt_id,
trigger_type=UiPathResumeTriggerType.API,
payload=UiPathConversationToolCallConfirmationValue(
tool_call_id=tool_call_id,
tool_name=approval_tool["name"],
input_schema=approval_tool["input_schema"],
input_value=approval_tool["input"],
),
),
],
)
return

# --- No approval needed — continue with phase 2 inline ---
# --- Continue with phase 2 inline ---

# --- Middleware: Summarization (second pass with tool results) ---
yield self._node_state("SummarizationMiddleware.before_model", S)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-dev"
version = "0.0.78"
version = "0.0.79"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand All @@ -10,7 +10,7 @@ dependencies = [
"pyperclip>=1.11.0, <2.0.0",
"fastapi>=0.128.8",
"uvicorn[standard]>=0.40.0",
"uipath>=2.10.0, <2.11.0",
"uipath>=2.10.57, <2.11.0",
"aiosqlite>=0.20.0",
"pywinpty>=2.0.0; sys_platform == 'win32'",
"mcp[cli]>=1.0.0",
Expand Down
2 changes: 0 additions & 2 deletions src/uipath/dev/models/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ def add(
role=self.get_role(event),
content_parts=[],
tool_calls=[],
interrupts=[],
created_at=self.get_timestamp(event),
updated_at=self.get_timestamp(event),
)
Expand Down Expand Up @@ -205,7 +204,6 @@ def get_user_message(user_text: str) -> UiPathConversationMessage:
)
],
tool_calls=[],
interrupts=[],
role="user",
)

Expand Down
14 changes: 0 additions & 14 deletions src/uipath/dev/models/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,3 @@ class ChatData:
run_id: str
event: UiPathConversationMessageEvent | None = None
message: UiPathConversationMessage | None = None


@dataclass
class InterruptData:
"""Plain data class for HITL interrupt events."""

run_id: str
interrupt_id: str
interrupt_type: str # "tool_call_confirmation" | "generic"
tool_call_id: str | None = None
tool_name: str | None = None
input_schema: Any | None = None
input_value: Any | None = None
content: Any | None = None
6 changes: 0 additions & 6 deletions src/uipath/dev/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

from uipath.dev.models.data import (
ChatData,
InterruptData,
LogData,
StateData,
TraceData,
Expand Down Expand Up @@ -85,7 +84,6 @@ def __init__(
on_trace=self._on_trace,
on_chat=self._on_chat,
on_state=self._on_state,
on_interrupt=self._on_interrupt,
debug_bridge_factory=lambda mode: WebDebugBridge(mode=mode),
on_run_removed=self.connection_manager.remove_run_subscriptions,
)
Expand Down Expand Up @@ -303,10 +301,6 @@ def _on_chat(self, chat_data: ChatData) -> None:
"""Broadcast chat message to subscribed WebSocket clients."""
self.connection_manager.broadcast_chat(chat_data)

def _on_interrupt(self, interrupt_data: InterruptData) -> None:
"""Broadcast chat interrupt to subscribed WebSocket clients."""
self.connection_manager.broadcast_interrupt(interrupt_data)

def _on_state(self, state_data: StateData) -> None:
"""Broadcast state transition to subscribed WebSocket clients."""
self.connection_manager.broadcast_state(state_data)
Expand Down
7 changes: 2 additions & 5 deletions src/uipath/dev/server/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useRunStore } from "./store/useRunStore";
import { useRunStore, projectToolCall } from "./store/useRunStore";
import { useAuthStore } from "./store/useAuthStore";
import { useConfigStore } from "./store/useConfigStore";
import { useWebSocket } from "./store/useWebSocket";
Expand Down Expand Up @@ -156,10 +156,7 @@ export default function App() {
.join("\n")
.trim() ?? "",
tool_calls: toolCalls.length > 0
? toolCalls.map((tc) => ({
name: (tc.name as string) ?? "",
has_result: !!tc.result,
}))
? toolCalls.map(projectToolCall)
: undefined,
};
});
Expand Down
14 changes: 12 additions & 2 deletions src/uipath/dev/server/frontend/src/api/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,18 @@ export class WsClient {
this.send("chat.message", { run_id: runId, text });
}

sendInterruptResponse(runId: string, data: Record<string, unknown>): void {
this.send("chat.interrupt_response", { run_id: runId, data });
sendConfirmToolCall(
runId: string,
toolCallId: string,
approved: boolean,
input?: unknown,
): void {
this.send("chat.confirm_tool_call", {
run_id: runId,
tool_call_id: toolCallId,
approved,
input,
});
}

debugStep(runId: string): void {
Expand Down
Loading
Loading