Skip to content

Commit d0e75b4

Browse files
authored
Merge branch 'main' into feat/model_settings_id
2 parents 68a7b37 + bb789ee commit d0e75b4

File tree

23 files changed

+1217
-105
lines changed

23 files changed

+1217
-105
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,5 @@ For additional commands related to linting, formatting, and building, run `just
104104
### General Rules
105105
- Please write informative description to the PRs for the reviewers to understand the context.
106106
- If you want to publish a `dev` build, please add a `build:dev` label to your PR.
107+
- You can then use the published `dev` version from the PR description/comments to use as needed <img width="880" height="433" alt="image" src="https://github.com/user-attachments/assets/b670bb08-a469-454f-a5f6-10d60477c033" />
108+

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
[project]
22
name = "uipath"
3-
version = "2.2.37"
3+
version = "2.3.0"
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"
77
dependencies = [
8-
"uipath-runtime>=0.2.7, <0.3.0",
98
"uipath-core>=0.1.4, <0.2.0",
9+
"uipath-runtime>=0.3.1, <0.4.0",
1010
"click>=8.3.1",
1111
"httpx>=0.28.1",
1212
"pyjwt>=2.10.1",

src/uipath/_cli/_chat/_bridge.py

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import logging
55
import os
6+
import uuid
67
from typing import Any
78
from urllib.parse import urlparse
89

@@ -12,7 +13,11 @@
1213
UiPathConversationEvent,
1314
UiPathConversationExchangeEndEvent,
1415
UiPathConversationExchangeEvent,
16+
UiPathConversationInterruptEvent,
17+
UiPathConversationInterruptStartEvent,
18+
UiPathConversationMessageEvent,
1519
)
20+
from uipath.runtime import UiPathRuntimeResult
1621
from uipath.runtime.chat import UiPathChatProtocol
1722
from uipath.runtime.context import UiPathRuntimeContext
1823

@@ -82,7 +87,6 @@ async def connect(self, timeout: float = 10.0) -> None:
8287
self._client.on("disconnect", self._handle_disconnect)
8388
self._client.on("connect_error", self._handle_connect_error)
8489

85-
# Clear connection event
8690
self._connected_event.clear()
8791

8892
try:
@@ -98,11 +102,8 @@ async def connect(self, timeout: float = 10.0) -> None:
98102
timeout=timeout,
99103
)
100104

101-
# Wait for connection confirmation
102105
await asyncio.wait_for(self._connected_event.wait(), timeout=timeout)
103106

104-
logger.info("WebSocket connection established successfully")
105-
106107
except asyncio.TimeoutError as e:
107108
error_message = (
108109
f"Failed to connect to WebSocket server within {timeout}s timeout"
@@ -127,34 +128,16 @@ async def disconnect(self) -> None:
127128
logger.warning("WebSocket client not connected")
128129
return
129130

130-
# Send exchange end event using stored IDs
131-
if self._client and self._connected_event.is_set():
132-
try:
133-
end_event = UiPathConversationEvent(
134-
conversation_id=self.conversation_id,
135-
exchange=UiPathConversationExchangeEvent(
136-
exchange_id=self.exchange_id,
137-
end=UiPathConversationExchangeEndEvent(),
138-
),
139-
)
140-
event_data = end_event.model_dump(
141-
mode="json", exclude_none=True, by_alias=True
142-
)
143-
await self._client.emit("ConversationEvent", event_data)
144-
logger.info("Exchange end event sent")
145-
except Exception as e:
146-
logger.warning(f"Error sending exchange end event: {e}")
147-
148131
try:
149-
logger.info("Disconnecting from WebSocket server")
150132
await self._client.disconnect()
151-
logger.info("WebSocket disconnected successfully")
152133
except Exception as e:
153134
logger.error(f"Error during WebSocket disconnect: {e}")
154135
finally:
155136
await self._cleanup_client()
156137

157-
async def emit_message_event(self, message_event: Any) -> None:
138+
async def emit_message_event(
139+
self, message_event: UiPathConversationMessageEvent
140+
) -> None:
158141
"""Wrap and send a message event to the WebSocket server.
159142
160143
Args:
@@ -183,14 +166,82 @@ async def emit_message_event(self, message_event: Any) -> None:
183166
mode="json", exclude_none=True, by_alias=True
184167
)
185168

186-
logger.debug("Sending conversation event to WebSocket")
187169
await self._client.emit("ConversationEvent", event_data)
188-
logger.debug("Conversation event sent successfully")
170+
171+
# Store the current message ID, used for emitting interrupt events.
172+
self._current_message_id = message_event.message_id
173+
174+
except Exception as e:
175+
logger.error(f"Error sending conversation event to WebSocket: {e}")
176+
raise RuntimeError(f"Failed to send conversation event: {e}") from e
177+
178+
async def emit_exchange_end_event(self) -> None:
179+
"""Send an exchange end event.
180+
181+
Raises:
182+
RuntimeError: If client is not connected
183+
"""
184+
if self._client is None:
185+
raise RuntimeError("WebSocket client not connected. Call connect() first.")
186+
187+
if not self._connected_event.is_set():
188+
raise RuntimeError("WebSocket client not in connected state")
189+
190+
try:
191+
exchange_end_event = UiPathConversationEvent(
192+
conversation_id=self.conversation_id,
193+
exchange=UiPathConversationExchangeEvent(
194+
exchange_id=self.exchange_id,
195+
end=UiPathConversationExchangeEndEvent(),
196+
),
197+
)
198+
199+
event_data = exchange_end_event.model_dump(
200+
mode="json", exclude_none=True, by_alias=True
201+
)
202+
203+
await self._client.emit("ConversationEvent", event_data)
189204

190205
except Exception as e:
191206
logger.error(f"Error sending conversation event to WebSocket: {e}")
192207
raise RuntimeError(f"Failed to send conversation event: {e}") from e
193208

209+
async def emit_interrupt_event(self, runtime_result: UiPathRuntimeResult):
210+
if self._client and self._connected_event.is_set():
211+
try:
212+
self._interrupt_id = str(uuid.uuid4())
213+
214+
interrupt_event = UiPathConversationEvent(
215+
conversation_id=self.conversation_id,
216+
exchange=UiPathConversationExchangeEvent(
217+
exchange_id=self.exchange_id,
218+
message=UiPathConversationMessageEvent(
219+
message_id=self._current_message_id,
220+
interrupt=UiPathConversationInterruptEvent(
221+
interrupt_id=self._interrupt_id,
222+
start=UiPathConversationInterruptStartEvent(
223+
type="coded-agent-interrupt",
224+
value=runtime_result.output,
225+
),
226+
),
227+
),
228+
),
229+
)
230+
event_data = interrupt_event.model_dump(
231+
mode="json", exclude_none=True, by_alias=True
232+
)
233+
await self._client.emit("ConversationEvent", event_data)
234+
except Exception as e:
235+
logger.warning(f"Error sending interrupt event: {e}")
236+
237+
async def wait_for_resume(self) -> dict[str, Any]:
238+
"""Wait for the interrupt_end event to be received.
239+
240+
Returns:
241+
Resume data from the interrupt end event
242+
"""
243+
return {}
244+
194245
@property
195246
def is_connected(self) -> bool:
196247
"""Check if the WebSocket is currently connected.

src/uipath/_cli/_debug/_bridge.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
UiPathRuntimeResult,
1919
UiPathRuntimeStatus,
2020
)
21-
from uipath.runtime.debug import UiPathDebugBridgeProtocol, UiPathDebugQuitError
21+
from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugQuitError
2222
from uipath.runtime.events import UiPathRuntimeStateEvent
2323
from uipath.runtime.resumable import UiPathResumeTriggerType
2424

@@ -846,7 +846,7 @@ async def _handle_error(self, error: Any) -> None:
846846
logger.error(f"SignalR error: {error}")
847847

848848

849-
def get_remote_debug_bridge(context: UiPathRuntimeContext) -> UiPathDebugBridgeProtocol:
849+
def get_remote_debug_bridge(context: UiPathRuntimeContext) -> UiPathDebugProtocol:
850850
"""Factory to get SignalR debug bridge for remote debugging."""
851851
uipath_url = os.environ.get("UIPATH_URL")
852852
if not uipath_url or not context.job_id:
@@ -869,7 +869,7 @@ def get_remote_debug_bridge(context: UiPathRuntimeContext) -> UiPathDebugBridgeP
869869

870870
def get_debug_bridge(
871871
context: UiPathRuntimeContext, verbose: bool = True
872-
) -> UiPathDebugBridgeProtocol:
872+
) -> UiPathDebugProtocol:
873873
"""Factory to get appropriate debug bridge based on context.
874874
875875
Args:
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
from pydantic import BaseModel, Field
1+
from uipath.agent.models.agent import ExampleCall
22

3-
4-
class ExampleCall(BaseModel):
5-
"""Example invocation for a resource."""
6-
7-
id: str = Field(..., alias="id")
8-
input: str = Field(..., alias="input")
9-
output: str = Field(..., alias="output")
3+
__all__ = ["ExampleCall"]

src/uipath/_cli/_evals/_progress_reporter.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,12 +537,57 @@ def _serialize_justification(
537537

538538
return justification
539539

540-
def _extract_agent_snapshot(self, entrypoint: str) -> StudioWebAgentSnapshot:
540+
def _extract_agent_snapshot(self, entrypoint: str | None) -> StudioWebAgentSnapshot:
541+
"""Extract agent snapshot from entry points configuration or low-code agent file.
542+
543+
For coded agents, reads from entry-points.json configuration file.
544+
For low-code agents (*.json files like agent.json), reads inputSchema
545+
and outputSchema directly from the agent file.
546+
547+
Args:
548+
entrypoint: The entrypoint file path to look up
549+
550+
Returns:
551+
StudioWebAgentSnapshot with input and output schemas
552+
"""
553+
if not entrypoint:
554+
logger.warning(
555+
"Entrypoint not provided - falling back to empty inputSchema "
556+
"and outputSchema"
557+
)
558+
return StudioWebAgentSnapshot(input_schema={}, output_schema={})
559+
541560
try:
561+
# Check if entrypoint is a low-code agent JSON file (e.g., agent.json)
562+
if entrypoint.endswith(".json"):
563+
agent_file_path = os.path.join(os.getcwd(), entrypoint)
564+
if os.path.exists(agent_file_path):
565+
with open(agent_file_path, "r") as f:
566+
agent_data = json.load(f)
567+
568+
# Low-code agent files have inputSchema and outputSchema at root
569+
input_schema = agent_data.get("inputSchema", {})
570+
output_schema = agent_data.get("outputSchema", {})
571+
572+
logger.debug(
573+
f"Extracted agent snapshot from low-code agent '{entrypoint}': "
574+
f"inputSchema={json.dumps(input_schema)}, "
575+
f"outputSchema={json.dumps(output_schema)}"
576+
)
577+
578+
return StudioWebAgentSnapshot(
579+
input_schema=input_schema, output_schema=output_schema
580+
)
581+
582+
# Fall back to entry-points.json for coded agents
542583
entry_points_file_path = os.path.join(
543584
os.getcwd(), str(UiPathConfig.entry_points_file_path)
544585
)
545586
if not os.path.exists(entry_points_file_path):
587+
logger.debug(
588+
f"Entry points file not found at {entry_points_file_path}, "
589+
"using empty schemas"
590+
)
546591
return StudioWebAgentSnapshot(input_schema={}, output_schema={})
547592

548593
with open(entry_points_file_path, "r") as f:
@@ -563,6 +608,12 @@ def _extract_agent_snapshot(self, entrypoint: str) -> StudioWebAgentSnapshot:
563608
input_schema = ep.get("input", {})
564609
output_schema = ep.get("output", {})
565610

611+
logger.debug(
612+
f"Extracted agent snapshot for entrypoint '{entrypoint}': "
613+
f"inputSchema={json.dumps(input_schema)}, "
614+
f"outputSchema={json.dumps(output_schema)}"
615+
)
616+
566617
return StudioWebAgentSnapshot(
567618
input_schema=input_schema, output_schema=output_schema
568619
)
@@ -724,6 +775,14 @@ def _update_eval_run_spec(
724775
# Both coded and legacy send payload directly at root level
725776
payload = inner_payload
726777

778+
# Log the payload for debugging eval run updates
779+
agent_type = "coded" if is_coded else "low-code"
780+
logger.debug(
781+
f"Updating eval run (type={agent_type}): "
782+
f"evalRunId={eval_run_id}, success={success}"
783+
)
784+
logger.debug(f"Full eval run update payload: {json.dumps(payload, indent=2)}")
785+
727786
return RequestSpec(
728787
method="PUT",
729788
endpoint=Endpoint(
@@ -762,6 +821,16 @@ def _update_coded_eval_run_spec(
762821
"evaluatorRuns": evaluator_runs,
763822
}
764823

824+
# Log the payload for debugging coded eval run updates
825+
agent_type = "coded" if is_coded else "low-code"
826+
logger.debug(
827+
f"Updating coded eval run (type={agent_type}): "
828+
f"evalRunId={eval_run_id}, success={success}"
829+
)
830+
logger.debug(
831+
f"Full coded eval run update payload: {json.dumps(payload, indent=2)}"
832+
)
833+
765834
return RequestSpec(
766835
method="PUT",
767836
endpoint=Endpoint(
@@ -826,6 +895,14 @@ def _create_eval_run_spec(
826895
# Both coded and legacy send payload directly at root level
827896
payload = inner_payload
828897

898+
# Log the payload for debugging eval run reporting
899+
agent_type = "coded" if is_coded else "low-code"
900+
logger.debug(
901+
f"Creating eval run (type={agent_type}): "
902+
f"evalSetRunId={eval_set_run_id}, evalItemId={eval_item.id}"
903+
)
904+
logger.debug(f"Full eval run payload: {json.dumps(payload, indent=2)}")
905+
829906
return RequestSpec(
830907
method="POST",
831908
endpoint=Endpoint(
@@ -872,6 +949,16 @@ def _create_eval_set_run_spec(
872949
# Both coded and legacy send payload directly at root level
873950
payload = inner_payload
874951

952+
# Log the payload for debugging eval set run reporting
953+
agent_type = "coded" if is_coded else "low-code"
954+
logger.info(
955+
f"Creating eval set run (type={agent_type}): "
956+
f"evalSetId={eval_set_id}, "
957+
f"inputSchema={json.dumps(payload.get('agentSnapshot', {}).get('inputSchema', {}))}, "
958+
f"outputSchema={json.dumps(payload.get('agentSnapshot', {}).get('outputSchema', {}))}"
959+
)
960+
logger.debug(f"Full eval set run payload: {json.dumps(payload, indent=2)}")
961+
875962
return RequestSpec(
876963
method="POST",
877964
endpoint=Endpoint(
@@ -926,6 +1013,17 @@ def _update_eval_set_run_spec(
9261013
# Both coded and legacy send payload directly at root level
9271014
payload = inner_payload
9281015

1016+
# Log the payload for debugging eval set run updates
1017+
agent_type = "coded" if is_coded else "low-code"
1018+
logger.info(
1019+
f"Updating eval set run (type={agent_type}): "
1020+
f"evalSetRunId={eval_set_run_id}, success={success}, "
1021+
f"evaluatorScores={json.dumps(payload.get('evaluatorScores', []))}"
1022+
)
1023+
logger.debug(
1024+
f"Full eval set run update payload: {json.dumps(payload, indent=2)}"
1025+
)
1026+
9291027
return RequestSpec(
9301028
method="PUT",
9311029
endpoint=Endpoint(

src/uipath/_cli/cli_debug.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
UiPathRuntimeProtocol,
1111
)
1212
from uipath.runtime.chat import UiPathChatProtocol, UiPathChatRuntime
13-
from uipath.runtime.debug import UiPathDebugBridgeProtocol, UiPathDebugRuntime
13+
from uipath.runtime.debug import UiPathDebugProtocol, UiPathDebugRuntime
1414

1515
from uipath._cli._chat._bridge import get_chat_bridge
1616
from uipath._cli._debug._bridge import get_debug_bridge
@@ -136,7 +136,7 @@ async def execute_debug_runtime():
136136
delegate=runtime, chat_bridge=chat_bridge
137137
)
138138

139-
debug_bridge: UiPathDebugBridgeProtocol = get_debug_bridge(ctx)
139+
debug_bridge: UiPathDebugProtocol = get_debug_bridge(ctx)
140140

141141
debug_runtime = UiPathDebugRuntime(
142142
delegate=chat_runtime or runtime,

0 commit comments

Comments
 (0)