Skip to content

Commit c482021

Browse files
authored
Merge pull request #180 from UiPath/fix/otel_tracing_run
fix: use otel for traces
2 parents cb88e0d + 784b7b6 commit c482021

File tree

19 files changed

+2357
-3047
lines changed

19 files changed

+2357
-3047
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "UiPath Langchain"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.10"
77
dependencies = [
8-
"uipath>=2.1.54, <2.2.0",
8+
"uipath>=2.1.56, <2.2.0",
99
"langgraph>=0.5.0, <0.7.0",
1010
"langchain-core>=0.3.34",
1111
"langgraph-checkpoint-sqlite>=2.0.3",

src/uipath_langchain/_cli/_runtime/_runtime.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from langchain_core.callbacks.base import BaseCallbackHandler
77
from langchain_core.messages import BaseMessage
88
from langchain_core.runnables.config import RunnableConfig
9-
from langchain_core.tracers.langchain import wait_for_all_tracers
109
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
1110
from langgraph.errors import EmptyInputError, GraphRecursionError, InvalidUpdateError
1211
from langgraph.graph.state import CompiledStateGraph
@@ -16,8 +15,6 @@
1615
UiPathRuntimeResult,
1716
)
1817

19-
from ..._utils import _instrument_traceable_attributes
20-
from ...tracers import AsyncUiPathTracer
2118
from .._utils._graph import LangGraphConfig
2219
from ._context import LangGraphRuntimeContext
2320
from ._conversation import map_message
@@ -48,13 +45,10 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
4845
Raises:
4946
LangGraphRuntimeError: If execution fails
5047
"""
51-
_instrument_traceable_attributes()
5248

5349
if self.context.state_graph is None:
5450
return None
5551

56-
tracer = None
57-
5852
try:
5953
async with AsyncSqliteSaver.from_conn_string(
6054
self.state_file_path
@@ -71,13 +65,8 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
7165

7266
processed_input = await input_processor.process()
7367

74-
# Set up tracing if available
7568
callbacks: List[BaseCallbackHandler] = []
7669

77-
if self.context.job_id and self.context.tracing_enabled:
78-
tracer = AsyncUiPathTracer(context=self.context.trace_context)
79-
callbacks = [tracer]
80-
8170
graph_config: RunnableConfig = {
8271
"configurable": {
8372
"thread_id": (
@@ -185,11 +174,7 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
185174
UiPathErrorCategory.USER,
186175
) from e
187176
finally:
188-
if tracer is not None:
189-
await tracer.wait_for_all_tracers()
190-
191-
if self.context.langsmith_tracing_enabled:
192-
wait_for_all_tracers()
177+
pass
193178

194179
async def validate(self) -> None:
195180
"""Validate runtime inputs."""

src/uipath_langchain/_cli/cli_dev.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from uipath._cli._utils._console import ConsoleLogger
1111
from uipath._cli.middlewares import MiddlewareResult
1212

13+
from .._tracing import _instrument_traceable_attributes
1314
from ._runtime._context import LangGraphRuntimeContext
1415
from ._runtime._runtime import LangGraphRuntime
1516

@@ -24,6 +25,8 @@ def langgraph_dev_middleware(interface: Optional[str]) -> MiddlewareResult:
2425
runtime_factory = UiPathRuntimeFactory(
2526
LangGraphRuntime, LangGraphRuntimeContext
2627
)
28+
29+
_instrument_traceable_attributes()
2730
runtime_factory.add_instrumentor(LangChainInstrumentor, get_current_span)
2831
app = UiPathDevTerminal(runtime_factory)
2932
asyncio.run(app.run_async())

src/uipath_langchain/_cli/cli_eval.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import asyncio
2-
from os import environ as env
32
from typing import List, Optional
43

4+
from openinference.instrumentation.langchain import (
5+
LangChainInstrumentor,
6+
get_current_span,
7+
)
58
from uipath._cli._evals._runtime import UiPathEvalContext, UiPathEvalRuntime
69
from uipath._cli._runtime._contracts import (
710
UiPathRuntimeFactory,
@@ -13,15 +16,15 @@
1316
from uipath_langchain._cli._runtime._context import LangGraphRuntimeContext
1417
from uipath_langchain._cli._runtime._runtime import LangGraphRuntime
1518
from uipath_langchain._cli._utils._graph import LangGraphConfig
19+
from uipath_langchain._tracing import (
20+
LangChainExporter,
21+
_instrument_traceable_attributes,
22+
)
1623

1724

1825
def langgraph_eval_middleware(
1926
entrypoint: Optional[str], eval_set: Optional[str], eval_ids: List[str], **kwargs
2027
) -> MiddlewareResult:
21-
# Add default env variables
22-
env["UIPATH_REQUESTING_PRODUCT"] = "uipath-python-sdk"
23-
env["UIPATH_REQUESTING_FEATURE"] = "langgraph-agent"
24-
2528
config = LangGraphConfig()
2629
if not config.exists:
2730
return MiddlewareResult(
@@ -33,6 +36,8 @@ def langgraph_eval_middleware(
3336
eval_context.eval_ids = eval_ids
3437

3538
try:
39+
_instrument_traceable_attributes()
40+
3641
runtime_entrypoint = entrypoint or auto_discover_entrypoint()
3742

3843
def generate_runtime_context(
@@ -53,6 +58,11 @@ def generate_runtime_context(
5358
),
5459
)
5560

61+
if eval_context.job_id:
62+
runtime_factory.add_span_exporter(LangChainExporter())
63+
64+
runtime_factory.add_instrumentor(LangChainInstrumentor, get_current_span)
65+
5666
async def execute():
5767
async with UiPathEvalRuntime.from_eval_context(
5868
factory=runtime_factory, context=eval_context

src/uipath_langchain/_cli/cli_run.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import asyncio
22
import os
3-
from os import environ as env
43
from typing import Optional
54

5+
from openinference.instrumentation.langchain import (
6+
LangChainInstrumentor,
7+
get_current_span,
8+
)
9+
from uipath._cli._runtime._contracts import (
10+
UiPathRuntimeFactory,
11+
)
612
from uipath._cli.middlewares import MiddlewareResult
713

14+
from .._tracing import LangChainExporter, _instrument_traceable_attributes
815
from ._runtime._exception import LangGraphRuntimeError
916
from ._runtime._runtime import ( # type: ignore[attr-defined]
1017
LangGraphRuntime,
@@ -24,23 +31,35 @@ def langgraph_run_middleware(
2431
) # Continue with normal flow if no langgraph.json
2532

2633
try:
27-
# Add default env variables
28-
env["UIPATH_REQUESTING_PRODUCT"] = "uipath-python-sdk"
29-
env["UIPATH_REQUESTING_FEATURE"] = "langgraph-agent"
30-
3134
context = LangGraphRuntimeContext.with_defaults(**kwargs)
3235
context.langgraph_config = config
3336
context.entrypoint = entrypoint
3437
context.input = input
3538
context.resume = resume
3639

40+
_instrument_traceable_attributes()
41+
42+
def generate_runtime(ctx: LangGraphRuntimeContext) -> LangGraphRuntime:
43+
runtime = LangGraphRuntime(ctx)
44+
# If not resuming and no job id, delete the previous state file
45+
if not ctx.resume and ctx.job_id is None:
46+
if os.path.exists(runtime.state_file_path):
47+
os.remove(runtime.state_file_path)
48+
return runtime
49+
3750
async def execute():
38-
async with LangGraphRuntime.from_context(context) as runtime:
39-
if context.resume is False and context.job_id is None:
40-
# Delete the previous graph state file at debug time
41-
if os.path.exists(runtime.state_file_path):
42-
os.remove(runtime.state_file_path)
43-
await runtime.execute()
51+
runtime_factory = UiPathRuntimeFactory(
52+
LangGraphRuntime,
53+
LangGraphRuntimeContext,
54+
runtime_generator=generate_runtime,
55+
)
56+
57+
if context.job_id:
58+
runtime_factory.add_span_exporter(LangChainExporter())
59+
60+
runtime_factory.add_instrumentor(LangChainInstrumentor, get_current_span)
61+
62+
await runtime_factory.execute(context)
4463

4564
asyncio.run(execute())
4665

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from ._instrument_traceable import _instrument_traceable_attributes
2+
from ._oteladapter import LangChainExporter
3+
4+
__all__ = ["LangChainExporter", "_instrument_traceable_attributes"]
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import importlib
2+
import logging
3+
import sys
4+
from typing import Any, Callable, Dict, List, Optional
5+
6+
from uipath.tracing import traced
7+
8+
# Original module and traceable function references
9+
original_langsmith: Any = None
10+
original_traceable: Any = None
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
# Apply the patch
16+
def _map_traceable_to_traced_args(
17+
run_type: Optional[str] = None,
18+
name: Optional[str] = None,
19+
tags: Optional[List[str]] = None,
20+
metadata: Optional[Dict[str, Any]] = None,
21+
**kwargs: Any,
22+
) -> Dict[str, Any]:
23+
"""
24+
Map LangSmith @traceable arguments to UiPath @traced() arguments.
25+
26+
Args:
27+
run_type: Function type (tool, chain, llm, retriever, etc.)
28+
name: Custom name for the traced function
29+
tags: List of tags for categorization
30+
metadata: Additional metadata dictionary
31+
**kwargs: Additional arguments (ignored)
32+
33+
Returns:
34+
Dict containing mapped arguments for @traced()
35+
"""
36+
traced_args = {}
37+
38+
# Direct mappings
39+
if name is not None:
40+
traced_args["name"] = name
41+
42+
# Pass through run_type directly to UiPath @traced()
43+
if run_type:
44+
traced_args["run_type"] = run_type
45+
46+
# For span_type, we can derive from run_type or use a default
47+
if run_type:
48+
# Map run_type to appropriate span_type for OpenTelemetry
49+
span_type_mapping = {
50+
"tool": "tool_call",
51+
"chain": "chain_execution",
52+
"llm": "llm_call",
53+
"retriever": "retrieval",
54+
"embedding": "embedding",
55+
"prompt": "prompt_template",
56+
"parser": "output_parser",
57+
}
58+
traced_args["span_type"] = span_type_mapping.get(run_type, run_type)
59+
60+
# Note: UiPath @traced() doesn't support custom attributes directly
61+
# Tags and metadata information is lost in the current mapping
62+
# This could be enhanced in future versions
63+
64+
return traced_args
65+
66+
67+
def otel_traceable_adapter(
68+
func: Optional[Callable[..., Any]] = None,
69+
*,
70+
run_type: Optional[str] = None,
71+
name: Optional[str] = None,
72+
tags: Optional[List[str]] = None,
73+
metadata: Optional[Dict[str, Any]] = None,
74+
**kwargs: Any,
75+
):
76+
"""
77+
OTEL-based adapter that converts LangSmith @traceable decorator calls to UiPath @traced().
78+
79+
This function maintains the same interface as LangSmith's @traceable but uses
80+
UiPath's OpenTelemetry-based tracing system underneath.
81+
82+
Args:
83+
func: Function to be decorated (when used without parentheses)
84+
run_type: Type of function (tool, chain, llm, etc.)
85+
name: Custom name for tracing
86+
tags: List of tags for categorization
87+
metadata: Additional metadata dictionary
88+
**kwargs: Additional arguments (for future compatibility)
89+
90+
Returns:
91+
Decorated function or decorator function
92+
"""
93+
94+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
95+
# Map arguments to @traced() format
96+
traced_args = _map_traceable_to_traced_args(
97+
run_type=run_type, name=name, tags=tags, metadata=metadata, **kwargs
98+
)
99+
100+
# Apply UiPath @traced() decorator
101+
return traced(**traced_args)(f)
102+
103+
# Handle both @traceable and @traceable(...) usage patterns
104+
if func is None:
105+
# Called as @traceable(...) - return decorator
106+
return decorator
107+
else:
108+
# Called as @traceable - apply decorator directly
109+
return decorator(func)
110+
111+
112+
def _instrument_traceable_attributes():
113+
"""Apply the patch to langsmith module at import time."""
114+
global original_langsmith, original_traceable
115+
116+
# Import the original module if not already done
117+
if original_langsmith is None:
118+
# Temporarily remove our custom module from sys.modules
119+
if "langsmith" in sys.modules:
120+
original_langsmith = sys.modules["langsmith"]
121+
del sys.modules["langsmith"]
122+
123+
# Import the original module
124+
original_langsmith = importlib.import_module("langsmith")
125+
126+
# Store the original traceable
127+
original_traceable = original_langsmith.traceable
128+
129+
# Replace the traceable function with our patched version
130+
original_langsmith.traceable = otel_traceable_adapter
131+
132+
# Put our modified module back
133+
sys.modules["langsmith"] = original_langsmith
134+
135+
return original_langsmith

0 commit comments

Comments
 (0)