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: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath"
version = "2.5.8"
version = "2.5.9"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
5 changes: 5 additions & 0 deletions src/uipath/_cli/_evals/_evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@

from uipath._cli._evals._runtime import UiPathEvalContext, UiPathEvalRuntime
from uipath._events._event_bus import EventBus
from uipath.tracing import LlmOpsHttpExporter


async def evaluate(
runtime_factory: UiPathRuntimeFactoryProtocol,
trace_manager: UiPathTraceManager,
eval_context: UiPathEvalContext,
event_bus: EventBus,
job_exporter: LlmOpsHttpExporter | None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not do this -- it's a breaking change for code conversion. Moving initialization logic to cli_eval will avoid this change..

studio_web_tracking_exporter: LlmOpsHttpExporter | None,
) -> UiPathRuntimeResult:
async with UiPathEvalRuntime(
factory=runtime_factory,
context=eval_context,
trace_manager=trace_manager,
event_bus=event_bus,
job_exporter=job_exporter,
studio_web_tracking_exporter=studio_web_tracking_exporter,
) as eval_runtime:
results = await eval_runtime.execute()
await event_bus.wait_for_all(timeout=10)
Expand Down
11 changes: 0 additions & 11 deletions src/uipath/_cli/_evals/_progress_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,17 +422,6 @@ async def handle_update_eval_run(self, payload: EvalRunUpdatedEvent) -> None:
try:
eval_run_id = self.eval_run_ids.get(payload.execution_id)

# Use evalRunId as the trace_id for agent execution spans
# This makes all agent spans children of the eval run trace
if eval_run_id:
self.spans_exporter.trace_id = eval_run_id
else:
# Fallback to evalSetRunId if eval_run_id not available yet
if self.eval_set_execution_id:
self.spans_exporter.trace_id = self.eval_set_run_ids.get(
self.eval_set_execution_id
)

self.spans_exporter.export(payload.spans)

for eval_result in payload.eval_results:
Expand Down
28 changes: 23 additions & 5 deletions src/uipath/_cli/_evals/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ def __init__(
factory: UiPathRuntimeFactoryProtocol,
trace_manager: UiPathTraceManager,
event_bus: EventBus,
job_exporter: LlmOpsHttpExporter | None,
studio_web_tracking_exporter: LlmOpsHttpExporter | None,
):
self.context: UiPathEvalContext = context
# Wrap the factory to support model settings overrides
Expand All @@ -322,11 +324,27 @@ def __init__(
self.trace_manager.tracer_span_processors.append(span_processor)
self.trace_manager.tracer_provider.add_span_processor(span_processor)

# Live tracking processor for real-time span updates
live_tracking_exporter = LlmOpsHttpExporter()
live_tracking_processor = LiveTrackingSpanProcessor(live_tracking_exporter)
self.trace_manager.tracer_span_processors.append(live_tracking_processor)
self.trace_manager.tracer_provider.add_span_processor(live_tracking_processor)
# Job exporter tracking processor for real-time span updates
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not do all of this in cli_eval.py instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While making the change, please move LiveTrackingSpanProcessor to a separate file..

if job_exporter:
self.job_exporter = job_exporter
job_tracking_processor = LiveTrackingSpanProcessor(self.job_exporter)
self.trace_manager.tracer_span_processors.append(job_tracking_processor)
self.trace_manager.tracer_provider.add_span_processor(
job_tracking_processor
)

# Studio Web tracking processor for real-time span updates
if studio_web_tracking_exporter:
self.studio_web_tracking_exporter = studio_web_tracking_exporter
studio_web_tracking_processor = LiveTrackingSpanProcessor(
self.studio_web_tracking_exporter
)
self.trace_manager.tracer_span_processors.append(
studio_web_tracking_processor
)
self.trace_manager.tracer_provider.add_span_processor(
studio_web_tracking_processor
)

self.logs_exporter: ExecutionLogsExporter = ExecutionLogsExporter()
# Use job_id if available (for single runtime runs), otherwise generate UUID
Expand Down
28 changes: 25 additions & 3 deletions src/uipath/_cli/cli_eval.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ast
import asyncio
import logging
import os
from typing import Any

Expand Down Expand Up @@ -28,6 +29,7 @@
from ._utils._console import ConsoleLogger
from ._utils._eval_set import EvalHelpers

logger = logging.getLogger(__name__)
console = ConsoleLogger()


Expand Down Expand Up @@ -203,8 +205,18 @@ def eval(
async def execute_eval():
event_bus = EventBus()

# Only create studio web exporter when reporting to Studio Web
studio_web_tracking_exporter = None
if should_register_progress_reporter:
progress_reporter = StudioWebProgressReporter(LlmOpsHttpExporter())
studio_web_tracking_exporter = LlmOpsHttpExporter()
if eval_context.eval_set_run_id:
studio_web_tracking_exporter.trace_id = (
eval_context.eval_set_run_id
)

progress_reporter = StudioWebProgressReporter(
studio_web_tracking_exporter
)
await progress_reporter.subscribe_to_eval_runtime_events(event_bus)

console_reporter = ConsoleProgressReporter()
Expand All @@ -223,8 +235,11 @@ async def execute_eval():
# Set job_id in eval context for single runtime runs
eval_context.job_id = ctx.job_id

# Create job exporter for live tracking
job_exporter = None
if ctx.job_id:
trace_manager.add_span_exporter(LlmOpsHttpExporter())
job_exporter = LlmOpsHttpExporter()
trace_manager.add_span_exporter(job_exporter)

if trace_file:
trace_manager.add_span_exporter(
Expand All @@ -247,11 +262,18 @@ async def execute_eval():
trace_manager,
eval_context,
event_bus,
job_exporter,
studio_web_tracking_exporter,
)
else:
# Fall back to execution without overwrites
ctx.result = await evaluate(
runtime_factory, trace_manager, eval_context, event_bus
runtime_factory,
trace_manager,
eval_context,
event_bus,
job_exporter,
studio_web_tracking_exporter,
)
finally:
if runtime_factory:
Expand Down
82 changes: 73 additions & 9 deletions tests/cli/eval/test_eval_runtime_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
UiPathEvalRuntime,
)
from uipath._events._event_bus import EventBus
from uipath.tracing import LlmOpsHttpExporter


class MockRuntimeSchema(UiPathRuntimeSchema):
Expand Down Expand Up @@ -164,7 +165,14 @@ async def create_runtime():
return BaseTestRuntime()

factory = MockFactory(create_runtime)
return UiPathEvalRuntime(context, factory, trace_manager, event_bus)
return UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

def test_finds_model_in_direct_runtime(self, eval_runtime):
"""Test finding agent model directly on runtime."""
Expand Down Expand Up @@ -228,7 +236,14 @@ async def create_runtime():
factory = MockFactory(create_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

runtime = await create_runtime()
model = await eval_runtime._get_agent_model(runtime)
Expand All @@ -243,7 +258,14 @@ async def create_runtime():
factory = MockFactory(create_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

runtime = await create_runtime()
model = await eval_runtime._get_agent_model(runtime)
Expand All @@ -258,7 +280,14 @@ async def create_runtime():
factory = MockFactory(create_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

runtime = await create_runtime()

Expand All @@ -277,7 +306,14 @@ async def create_good_runtime():
factory = MockFactory(create_good_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

# Create a bad runtime that raises during get_agent_model
class BadRuntime(BaseTestRuntime):
Expand Down Expand Up @@ -310,7 +346,14 @@ async def create_runtime():
factory = MockFactory(create_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

runtime = await create_runtime()
schema = await eval_runtime.get_schema(runtime)
Expand All @@ -326,7 +369,14 @@ async def create_runtime():
factory = MockFactory(create_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

runtime = await create_runtime()

Expand All @@ -346,7 +396,14 @@ async def create_runtime():
factory = MockFactory(create_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

runtime = await create_runtime()

Expand Down Expand Up @@ -393,7 +450,14 @@ async def create_runtime():
factory = MockFactory(create_runtime)
event_bus = EventBus()
trace_manager = UiPathTraceManager()
eval_runtime = UiPathEvalRuntime(context, factory, trace_manager, event_bus)
eval_runtime = UiPathEvalRuntime(
context,
factory,
trace_manager,
event_bus,
LlmOpsHttpExporter(),
LlmOpsHttpExporter(),
)

model = await eval_runtime._get_agent_model(resumable_runtime)
assert model == "gpt-4o-from-agent-json"
Loading
Loading