Skip to content
Merged
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-runtime"
version = "0.11.3"
version = "0.11.4"
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
37 changes: 36 additions & 1 deletion src/uipath/runtime/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pathlib import Path
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field, model_validator
from uipath.core.errors import UiPathFaultedTriggerError
from uipath.core.tracing import UiPathTraceManager

Expand All @@ -23,6 +23,13 @@

logger = logging.getLogger(__name__)

_EXECUTION_SOURCE_BY_COMMAND: dict[str, str] = {
"run": "runtime",
"debug": "playground",
"dev": "playground",
"eval": "eval",
}


class UiPathRuntimeContext(BaseModel):
"""Context information passed throughout the runtime execution."""
Expand All @@ -31,6 +38,9 @@ class UiPathRuntimeContext(BaseModel):
input: str | None = None
resume: bool = False
command: str | None = None
execution_source: str | None = Field(
None, description="Execution source derived from the command."
)
job_id: str | None = None
conversation_id: str | None = Field(
None, description="Conversation identifier for CAS"
Expand Down Expand Up @@ -96,6 +106,28 @@ class UiPathRuntimeContext(BaseModel):

model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")

def _apply_execution_source(self) -> None:
"""Derive execution_source from the command, if not already set.

Only assigns a mapped value, so the field stays unset (absent under
``model_dump(exclude_unset=True)``) for unmapped commands, and an
explicitly-provided value is never overwritten.
"""
if self.execution_source is None and self.command is not None:
source = _EXECUTION_SOURCE_BY_COMMAND.get(self.command)
if source is not None:
self.execution_source = source

@model_validator(mode="after")
def _derive_execution_source(self) -> "UiPathRuntimeContext":
"""Derive execution_source on the constructor path (e.g. dev/init).

``with_defaults`` mutates ``command`` via ``setattr`` after construction,
so it re-applies the derivation itself.
"""
self._apply_execution_source()
return self

def get_input(self) -> dict[str, Any] | None:
"""Get parsed input data.

Expand Down Expand Up @@ -345,6 +377,9 @@ def with_defaults(
for k, v in kwargs.items():
setattr(base, k, v)

# setattr does not re-run the validator, so derive explicitly.
base._apply_execution_source()

return base

@classmethod
Expand Down
53 changes: 53 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,56 @@ def test_string_output_wrapped_in_dict() -> None:

assert result_dict["output"] == {"output": "primitive str"}
assert result_dict["status"] == UiPathRuntimeStatus.SUCCESSFUL


@pytest.mark.parametrize(
"command,expected",
[
("run", "runtime"),
("debug", "playground"),
("dev", "playground"),
("eval", "eval"),
],
)
def test_constructor_derives_execution_source(command: str, expected: str) -> None:
"""execution_source is derived from the command on the plain constructor path."""
ctx = UiPathRuntimeContext(command=command)

assert ctx.execution_source == expected


@pytest.mark.parametrize(
"command,expected",
[
("run", "runtime"),
("eval", "eval"),
],
)
def test_with_defaults_derives_execution_source(
command: str, expected: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""execution_source is also derived via with_defaults."""
monkeypatch.chdir(tmp_path)

ctx = UiPathRuntimeContext.with_defaults(command=command)

assert ctx.execution_source == expected


def test_execution_source_unset_for_unmapped_command() -> None:
"""Commands that do not run an agent leave execution_source unset.

The field must remain unset (not explicitly None) so it is absent from
model_dump(exclude_unset=True).
"""
ctx = UiPathRuntimeContext(command="pack")

assert ctx.execution_source is None
assert "execution_source" not in ctx.model_dump(exclude_unset=True)


def test_explicit_execution_source_not_overwritten() -> None:
"""An explicitly provided execution_source takes precedence over the command."""
ctx = UiPathRuntimeContext(command="run", execution_source="custom")

assert ctx.execution_source == "custom"
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading