Skip to content

Commit e86087f

Browse files
authored
Merge pull request #8 from UiPath/fix/reactive_ui
fix: use runtime factory protocol for entrypoints and schema
2 parents 1607b74 + 99563b9 commit e86087f

File tree

7 files changed

+871
-319
lines changed

7 files changed

+871
-319
lines changed

src/uipath/dev/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def compose(self) -> ComposeResult:
8383
yield NewRunPanel(
8484
id="new-run-panel",
8585
classes="new-run-panel",
86+
runtime_factory=self.runtime_factory,
8687
)
8788

8889
# Run details panel (initially hidden)
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import asyncio
2+
import logging
3+
from typing import Any, AsyncGenerator, Optional
4+
5+
from opentelemetry import trace
6+
from uipath.runtime import (
7+
UiPathExecuteOptions,
8+
UiPathRuntimeEvent,
9+
UiPathRuntimeResult,
10+
UiPathRuntimeStatus,
11+
UiPathStreamOptions,
12+
)
13+
from uipath.runtime.schema import UiPathRuntimeSchema
14+
15+
ENTRYPOINT_CONTEXT = "agent/context.py:run"
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class MockContextRuntime:
21+
"""A mock runtime that simulates a multi-step workflow with rich telemetry."""
22+
23+
def __init__(self, entrypoint: str = ENTRYPOINT_CONTEXT) -> None:
24+
self.entrypoint = entrypoint
25+
self.tracer = trace.get_tracer("uipath.dev.mock.context")
26+
27+
async def get_schema(self) -> UiPathRuntimeSchema:
28+
return UiPathRuntimeSchema(
29+
filePath=self.entrypoint,
30+
uniqueId="mock-runtime",
31+
type="agent",
32+
input={
33+
"type": "object",
34+
"properties": {"message": {"type": "string"}},
35+
"required": ["message"],
36+
},
37+
output={
38+
"type": "object",
39+
"properties": {"result": {"type": "string"}},
40+
"required": ["result"],
41+
},
42+
)
43+
44+
async def execute(
45+
self,
46+
input: Optional[dict[str, Any]] = None,
47+
options: Optional[UiPathExecuteOptions] = None,
48+
) -> UiPathRuntimeResult:
49+
payload = input or {}
50+
51+
entrypoint = "mock-entrypoint"
52+
message = str(payload.get("message", ""))
53+
message_length = len(message)
54+
55+
with self.tracer.start_as_current_span(
56+
"mock-runtime.execute",
57+
attributes={
58+
"uipath.runtime.name": "MockRuntime",
59+
"uipath.runtime.type": "agent",
60+
"uipath.runtime.entrypoint": entrypoint,
61+
"uipath.input.message.length": message_length,
62+
"uipath.input.has_message": "message" in payload,
63+
},
64+
) as root_span:
65+
logger.info(
66+
"MockRuntime: starting execution",
67+
extra={
68+
"uipath.runtime.entrypoint": entrypoint,
69+
},
70+
)
71+
print(f"[MockRuntime] Starting execution with payload={payload!r}")
72+
73+
# Stage 1: Initialization
74+
with self.tracer.start_as_current_span(
75+
"initialize.environment",
76+
attributes={
77+
"uipath.step.name": "initialize-environment",
78+
"uipath.step.kind": "init",
79+
},
80+
):
81+
logger.info("MockRuntime: initializing environment")
82+
print("[MockRuntime] Initializing environment...")
83+
await asyncio.sleep(0.5)
84+
85+
# Stage 2: Validation
86+
with self.tracer.start_as_current_span(
87+
"validate.input",
88+
attributes={
89+
"uipath.step.name": "validate-input",
90+
"uipath.step.kind": "validation",
91+
"uipath.input.has_message": "message" in payload,
92+
},
93+
) as validate_span:
94+
logger.info("MockRuntime: validating input")
95+
print("[MockRuntime] Validating input...")
96+
await asyncio.sleep(0.5)
97+
98+
if "message" not in payload:
99+
logger.warning("MockRuntime: missing 'message' in payload")
100+
validate_span.set_attribute(
101+
"uipath.validation.missing_field", "message"
102+
)
103+
104+
# Stage 3: Preprocessing
105+
with self.tracer.start_as_current_span(
106+
"preprocess.data",
107+
attributes={
108+
"uipath.step.name": "preprocess-data",
109+
"uipath.step.kind": "preprocess",
110+
"uipath.input.size.bytes": len(str(payload).encode("utf-8")),
111+
},
112+
):
113+
logger.info("MockRuntime: preprocessing data")
114+
print("[MockRuntime] Preprocessing data...")
115+
await asyncio.sleep(0.5)
116+
117+
# Stage 4: Compute / reasoning
118+
with self.tracer.start_as_current_span(
119+
"compute.result",
120+
attributes={
121+
"uipath.step.name": "compute-result",
122+
"uipath.step.kind": "compute",
123+
},
124+
):
125+
logger.info("MockRuntime: compute phase started")
126+
print("[MockRuntime] Compute phase...")
127+
128+
# Subtask: embedding computation
129+
with self.tracer.start_as_current_span(
130+
"compute.embeddings",
131+
attributes={
132+
"uipath.step.name": "compute-embeddings",
133+
"uipath.step.kind": "compute-subtask",
134+
},
135+
):
136+
logger.info("MockRuntime: computing embeddings")
137+
print("[MockRuntime] Computing embeddings...")
138+
await asyncio.sleep(0.5)
139+
140+
# Subtask: KB query
141+
with self.tracer.start_as_current_span(
142+
"query.knowledgebase",
143+
attributes={
144+
"uipath.step.name": "query-knowledgebase",
145+
"uipath.step.kind": "io",
146+
"uipath.kb.query.length": message_length,
147+
},
148+
):
149+
logger.info("MockRuntime: querying knowledge base")
150+
print("[MockRuntime] Querying knowledge base...")
151+
await asyncio.sleep(0.5)
152+
153+
# Stage 5: Post-processing
154+
with self.tracer.start_as_current_span(
155+
"postprocess.results",
156+
attributes={
157+
"uipath.step.name": "postprocess-results",
158+
"uipath.step.kind": "postprocess",
159+
},
160+
):
161+
logger.info("MockRuntime: post-processing results")
162+
print("[MockRuntime] Post-processing results...")
163+
await asyncio.sleep(0.4)
164+
165+
with self.tracer.start_as_current_span(
166+
"generate.output",
167+
attributes={
168+
"uipath.step.name": "generate-output",
169+
"uipath.step.kind": "postprocess-subtask",
170+
},
171+
):
172+
logger.info("MockRuntime: generating structured output")
173+
print("[MockRuntime] Generating output...")
174+
await asyncio.sleep(0.4)
175+
176+
# Stage 6: Persistence
177+
with self.tracer.start_as_current_span(
178+
"persist.artifacts",
179+
attributes={
180+
"uipath.step.name": "persist-artifacts",
181+
"uipath.step.kind": "io",
182+
"uipath.persistence.enabled": False,
183+
},
184+
):
185+
logger.info("MockRuntime: persisting artifacts (mock)")
186+
print("[MockRuntime] Persisting artifacts (mock)...")
187+
await asyncio.sleep(0.4)
188+
189+
# Stage 7: Cleanup
190+
with self.tracer.start_as_current_span(
191+
"cleanup.resources",
192+
attributes={
193+
"uipath.step.name": "cleanup-resources",
194+
"uipath.step.kind": "cleanup",
195+
},
196+
):
197+
logger.info("MockRuntime: cleaning up resources")
198+
print("[MockRuntime] Cleaning up resources...")
199+
await asyncio.sleep(0.3)
200+
201+
result_payload = {
202+
"result": f"Mock runtime processed: {payload.get('message', '<no message>')}",
203+
"metadata": {
204+
"entrypoint": entrypoint,
205+
"message_length": message_length,
206+
},
207+
}
208+
209+
root_span.set_attribute("uipath.runtime.status", "success")
210+
root_span.set_attribute("uipath.runtime.duration.approx", "5s")
211+
root_span.set_attribute("uipath.output.has_error", False)
212+
root_span.set_attribute(
213+
"uipath.output.message_length", len(str(result_payload))
214+
)
215+
216+
logger.info(
217+
"MockRuntime: execution completed successfully",
218+
extra={
219+
"uipath.runtime.status": "success",
220+
},
221+
)
222+
print(f"[MockRuntime] Finished successfully with result={result_payload!r}")
223+
224+
return UiPathRuntimeResult(
225+
output=result_payload,
226+
status=UiPathRuntimeStatus.SUCCESSFUL,
227+
)
228+
229+
async def stream(
230+
self,
231+
input: Optional[dict[str, Any]] = None,
232+
options: Optional[UiPathStreamOptions] = None,
233+
) -> AsyncGenerator[UiPathRuntimeEvent, None]:
234+
logger.info("MockRuntime: stream() invoked")
235+
print("[MockRuntime] stream() invoked")
236+
yield await self.execute(input=input, options=options)
237+
238+
async def dispose(self) -> None:
239+
logger.info("MockRuntime: dispose() invoked")
240+
print("[MockRuntime] dispose() invoked")
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import asyncio
2+
import logging
3+
from typing import Any, AsyncGenerator, Optional
4+
5+
from opentelemetry import trace
6+
from uipath.runtime import (
7+
UiPathExecuteOptions,
8+
UiPathRuntimeEvent,
9+
UiPathRuntimeResult,
10+
UiPathRuntimeStatus,
11+
UiPathStreamOptions,
12+
)
13+
from uipath.runtime.schema import UiPathRuntimeSchema
14+
15+
ENTRYPOINT_GREETING = "agent/greeting.py:main"
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class MockGreetingRuntime:
21+
"""Mock runtime that builds a greeting and simulates a small pipeline."""
22+
23+
def __init__(self, entrypoint: str = ENTRYPOINT_GREETING) -> None:
24+
self.entrypoint = entrypoint
25+
self.tracer = trace.get_tracer("uipath.dev.mock.greeting")
26+
27+
async def get_schema(self) -> UiPathRuntimeSchema:
28+
return UiPathRuntimeSchema(
29+
filePath=self.entrypoint,
30+
uniqueId="mock-greeting-runtime",
31+
type="agent",
32+
input={
33+
"type": "object",
34+
"properties": {
35+
"name": {"type": "string", "description": "Who to greet"},
36+
"excited": {
37+
"type": "boolean",
38+
"description": "Whether to use an excited greeting",
39+
"default": True,
40+
},
41+
},
42+
"required": ["name"],
43+
},
44+
output={
45+
"type": "object",
46+
"properties": {
47+
"greeting": {"type": "string"},
48+
"metadata": {
49+
"type": "object",
50+
"properties": {
51+
"uppercase": {"type": "boolean"},
52+
"length": {"type": "integer"},
53+
},
54+
},
55+
},
56+
"required": ["greeting"],
57+
},
58+
)
59+
60+
async def execute(
61+
self,
62+
input: Optional[dict[str, Any]] = None,
63+
options: Optional[UiPathExecuteOptions] = None,
64+
) -> UiPathRuntimeResult:
65+
payload = input or {}
66+
name = str(payload.get("name", "world")).strip() or "world"
67+
excited = bool(payload.get("excited", True))
68+
69+
with self.tracer.start_as_current_span(
70+
"greeting.execute",
71+
attributes={
72+
"uipath.runtime.name": "GreetingRuntime",
73+
"uipath.runtime.type": "agent",
74+
"uipath.runtime.entrypoint": self.entrypoint,
75+
"uipath.input.name": name,
76+
"uipath.input.excited": excited,
77+
},
78+
):
79+
logger.info("GreetingRuntime: starting execution")
80+
81+
# Stage 1 - normalize name
82+
with self.tracer.start_as_current_span(
83+
"greeting.normalize_name",
84+
attributes={"uipath.step.kind": "preprocess"},
85+
):
86+
await asyncio.sleep(0.1)
87+
normalized = name.title()
88+
89+
# Stage 2 - build greeting
90+
with self.tracer.start_as_current_span(
91+
"greeting.build_message",
92+
attributes={"uipath.step.kind": "compute"},
93+
):
94+
await asyncio.sleep(0.1)
95+
greeting = f"Hello, {normalized}!"
96+
if excited:
97+
greeting += " Excited to meet you!"
98+
99+
# Stage 3 - compute metadata
100+
with self.tracer.start_as_current_span(
101+
"greeting.compute_metadata",
102+
attributes={"uipath.step.kind": "postprocess"},
103+
):
104+
await asyncio.sleep(0.05)
105+
metadata = {
106+
"uppercase": greeting.isupper(),
107+
"length": len(greeting),
108+
}
109+
110+
result_payload = {
111+
"greeting": greeting,
112+
"metadata": metadata,
113+
}
114+
115+
logger.info("GreetingRuntime: execution completed", extra=metadata)
116+
117+
return UiPathRuntimeResult(
118+
output=result_payload,
119+
status=UiPathRuntimeStatus.SUCCESSFUL,
120+
)
121+
122+
async def stream(
123+
self,
124+
input: Optional[dict[str, Any]] = None,
125+
options: Optional[UiPathStreamOptions] = None,
126+
) -> AsyncGenerator[UiPathRuntimeEvent, None]:
127+
logger.info("GreetingRuntime: stream() invoked")
128+
yield await self.execute(input=input, options=options)
129+
130+
async def dispose(self) -> None:
131+
logger.info("GreetingRuntime: dispose() invoked")

0 commit comments

Comments
 (0)