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-langchain"
version = "0.18.15"
version = "0.18.16"
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
28 changes: 22 additions & 6 deletions src/uipath_langchain/runtime/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ def reset(token: Any) -> None:

ReferenceContext: Any = None
ReferenceContextAccessor: Any = _NoopReferenceContextAccessor
resolve_project_id: Any = None

try:
from uipath.platform.common._span_utils import resolve_project_id
from uipath.tracing import (
Comment thread
jepadil23 marked this conversation as resolved.
ReferenceContext,
ReferenceContextAccessor,
Expand Down Expand Up @@ -117,19 +119,33 @@ def _push_reference_context(self) -> "contextvars.Token[Any]":
``langgraph`` entry for this runtime. Returns the ContextVar token
so the caller can reset in a ``finally`` block.

The entry must carry the id the span's ReferenceId resolves to, so the
hierarchy leaf and ReferenceId name the same entity. ``_SpanUtils``
derives ReferenceId as ``agentId or referenceId`` and overwrites
``agentId`` with ``resolve_project_id()`` — so that is the id to push,
with ``UIPATH_AGENT_ID`` as the fallback for when it resolves to nothing.

Returns a no-op token when the installed uipath package predates
reference-context support.
"""
if ReferenceContext is None:
return ReferenceContextAccessor.set(None)
agent_id = os.environ.get("UIPATH_AGENT_ID")
env_agent_id = os.environ.get("UIPATH_AGENT_ID")
hierarchy_id = (
resolve_project_id() if resolve_project_id else None
) or env_agent_id
agent_version = os.environ.get("UIPATH_PROCESS_VERSION") or None
parent_ctx = ReferenceContextAccessor.get() or ReferenceContext.Empty
ref_ctx = (
parent_ctx.add("langgraph", agent_id, agent_version)
if agent_id
else parent_ctx
)
try:
ref_ctx = (
parent_ctx.add("langgraph", hierarchy_id, agent_version)
if hierarchy_id
else parent_ctx
)
except ValueError:
# Not a UUID (env-var sources are unvalidated). Skip this entry
# rather than failing the run.
ref_ctx = parent_ctx
return ReferenceContextAccessor.set(ref_ctx)

async def execute(
Expand Down
71 changes: 71 additions & 0 deletions tests/runtime/test_reference_context_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,74 @@ def _boom(s: _S) -> _S:
pass

assert ReferenceContextAccessor.get() is None


# ---------------------------------------------------------------------------
# The hierarchy leaf must name the same entity as the span's ReferenceId
# ---------------------------------------------------------------------------


class TestHierarchyLeafMatchesReferenceId:
"""The pushed entry carries the id `ReferenceId` resolves to.

`_SpanUtils.otel_span_to_uipath_span` derives `ReferenceId` as
``agentId or referenceId`` and sets the `agentId` attribute from
``resolve_project_id()`` — which prefers ``uipath.json#id`` over
``UIPATH_AGENT_ID``. Pushing the raw env var left the hierarchy leaf naming
a different entity than `ReferenceId` whenever the two disagreed.
"""

ENV_AGENT_ID = "550e8400-e29b-41d4-a716-446655440020"
PROJECT_ID = "550e8400-e29b-41d4-a716-4466554400ff"

def setup_method(self) -> None:
_clear_accessor()

def teardown_method(self) -> None:
_clear_accessor()

def _leaf_id(self, tmp_path: Any, monkeypatch: pytest.MonkeyPatch) -> Any:
from uipath.platform.common._span_utils import _read_config_id

_read_config_id.cache_clear()
runtime = UiPathLangGraphRuntime(graph=_build_graph().compile())
token = runtime._push_reference_context()
try:
ctx = ReferenceContextAccessor.get()
return ctx.entries[-1].reference_id if ctx and len(ctx) else None
finally:
ReferenceContextAccessor.reset(token)
_read_config_id.cache_clear()

def test_uipath_json_id_wins_over_env_agent_id(
self, tmp_path: Any, monkeypatch: pytest.MonkeyPatch
) -> None:
"""resolve_project_id() prefers uipath.json#id, so the entry must too."""
import json

monkeypatch.delenv("UIPATH_PROCESS_VERSION", raising=False)
monkeypatch.setenv("UIPATH_AGENT_ID", self.ENV_AGENT_ID)
(tmp_path / "uipath.json").write_text(json.dumps({"id": self.PROJECT_ID}))
monkeypatch.chdir(tmp_path)

assert self._leaf_id(tmp_path, monkeypatch) == self.PROJECT_ID

def test_falls_back_to_env_agent_id(
self, tmp_path: Any, monkeypatch: pytest.MonkeyPatch
) -> None:
"""With no uipath.json, UIPATH_AGENT_ID is what ReferenceId resolves to."""
monkeypatch.delenv("UIPATH_PROCESS_VERSION", raising=False)
monkeypatch.setenv("UIPATH_AGENT_ID", self.ENV_AGENT_ID)
monkeypatch.chdir(tmp_path)

assert self._leaf_id(tmp_path, monkeypatch) == self.ENV_AGENT_ID

def test_non_uuid_id_is_skipped_not_raised(
self, tmp_path: Any, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Env-var sources are unvalidated; a bad id must not fail the run."""
monkeypatch.delenv("UIPATH_PROCESS_VERSION", raising=False)
monkeypatch.setenv("UIPATH_AGENT_ID", "not-a-uuid")
monkeypatch.chdir(tmp_path)

assert self._leaf_id(tmp_path, monkeypatch) is None
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