diff --git a/pyproject.toml b/pyproject.toml index c1427ea..ec055aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/uipath/runtime/context.py b/src/uipath/runtime/context.py index c655a76..924548c 100644 --- a/src/uipath/runtime/context.py +++ b/src/uipath/runtime/context.py @@ -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 @@ -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.""" @@ -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" @@ -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. @@ -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 diff --git a/tests/test_context.py b/tests/test_context.py index d4868c7..d3ed4ae 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -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" diff --git a/uv.lock b/uv.lock index 6b5443c..0a07a22 100644 --- a/uv.lock +++ b/uv.lock @@ -1012,7 +1012,7 @@ wheels = [ [[package]] name = "uipath-runtime" -version = "0.11.3" +version = "0.11.4" source = { editable = "." } dependencies = [ { name = "uipath-core" },