uipath-langchain 0.0.131__py3-none-any.whl → 0.0.132__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of uipath-langchain might be problematic. Click here for more details.
- uipath_langchain/_cli/_runtime/_input.py +0 -2
- uipath_langchain/_cli/_runtime/_runtime.py +1 -16
- uipath_langchain/_cli/cli_dev.py +3 -0
- uipath_langchain/_cli/cli_eval.py +15 -5
- uipath_langchain/_cli/cli_run.py +30 -11
- uipath_langchain/_tracing/__init__.py +4 -0
- uipath_langchain/_tracing/_instrument_traceable.py +135 -0
- uipath_langchain/_tracing/_oteladapter.py +222 -0
- uipath_langchain/{tracers → _tracing}/_utils.py +0 -24
- uipath_langchain/_utils/__init__.py +1 -2
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.132.dist-info}/METADATA +2 -2
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.132.dist-info}/RECORD +15 -16
- uipath_langchain/tracers/AsyncUiPathTracer.py +0 -274
- uipath_langchain/tracers/__init__.py +0 -7
- uipath_langchain/tracers/_events.py +0 -33
- uipath_langchain/tracers/_instrument_traceable.py +0 -416
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.132.dist-info}/WHEEL +0 -0
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.132.dist-info}/entry_points.txt +0 -0
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.132.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,7 +2,6 @@ import logging
|
|
|
2
2
|
from typing import Any, Optional, cast
|
|
3
3
|
|
|
4
4
|
from langgraph.types import Command
|
|
5
|
-
from uipath import UiPath
|
|
6
5
|
from uipath._cli._runtime._contracts import (
|
|
7
6
|
UiPathApiTrigger,
|
|
8
7
|
UiPathErrorCategory,
|
|
@@ -32,7 +31,6 @@ class LangGraphInputProcessor:
|
|
|
32
31
|
context: The runtime context for the graph execution.
|
|
33
32
|
"""
|
|
34
33
|
self.context = context
|
|
35
|
-
self.uipath = UiPath()
|
|
36
34
|
|
|
37
35
|
async def process(self) -> Any:
|
|
38
36
|
"""
|
|
@@ -6,7 +6,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
6
6
|
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
7
7
|
from langchain_core.messages import BaseMessage
|
|
8
8
|
from langchain_core.runnables.config import RunnableConfig
|
|
9
|
-
from langchain_core.tracers.langchain import wait_for_all_tracers
|
|
10
9
|
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
11
10
|
from langgraph.errors import EmptyInputError, GraphRecursionError, InvalidUpdateError
|
|
12
11
|
from langgraph.graph.state import CompiledStateGraph
|
|
@@ -16,8 +15,6 @@ from uipath._cli._runtime._contracts import (
|
|
|
16
15
|
UiPathRuntimeResult,
|
|
17
16
|
)
|
|
18
17
|
|
|
19
|
-
from ..._utils import _instrument_traceable_attributes
|
|
20
|
-
from ...tracers import AsyncUiPathTracer
|
|
21
18
|
from .._utils._graph import LangGraphConfig
|
|
22
19
|
from ._context import LangGraphRuntimeContext
|
|
23
20
|
from ._conversation import map_message
|
|
@@ -48,13 +45,10 @@ class LangGraphRuntime(UiPathBaseRuntime):
|
|
|
48
45
|
Raises:
|
|
49
46
|
LangGraphRuntimeError: If execution fails
|
|
50
47
|
"""
|
|
51
|
-
_instrument_traceable_attributes()
|
|
52
48
|
|
|
53
49
|
if self.context.state_graph is None:
|
|
54
50
|
return None
|
|
55
51
|
|
|
56
|
-
tracer = None
|
|
57
|
-
|
|
58
52
|
try:
|
|
59
53
|
async with AsyncSqliteSaver.from_conn_string(
|
|
60
54
|
self.state_file_path
|
|
@@ -71,13 +65,8 @@ class LangGraphRuntime(UiPathBaseRuntime):
|
|
|
71
65
|
|
|
72
66
|
processed_input = await input_processor.process()
|
|
73
67
|
|
|
74
|
-
# Set up tracing if available
|
|
75
68
|
callbacks: List[BaseCallbackHandler] = []
|
|
76
69
|
|
|
77
|
-
if self.context.job_id and self.context.tracing_enabled:
|
|
78
|
-
tracer = AsyncUiPathTracer(context=self.context.trace_context)
|
|
79
|
-
callbacks = [tracer]
|
|
80
|
-
|
|
81
70
|
graph_config: RunnableConfig = {
|
|
82
71
|
"configurable": {
|
|
83
72
|
"thread_id": (
|
|
@@ -185,11 +174,7 @@ class LangGraphRuntime(UiPathBaseRuntime):
|
|
|
185
174
|
UiPathErrorCategory.USER,
|
|
186
175
|
) from e
|
|
187
176
|
finally:
|
|
188
|
-
|
|
189
|
-
await tracer.wait_for_all_tracers()
|
|
190
|
-
|
|
191
|
-
if self.context.langsmith_tracing_enabled:
|
|
192
|
-
wait_for_all_tracers()
|
|
177
|
+
pass
|
|
193
178
|
|
|
194
179
|
async def validate(self) -> None:
|
|
195
180
|
"""Validate runtime inputs."""
|
uipath_langchain/_cli/cli_dev.py
CHANGED
|
@@ -10,6 +10,7 @@ from uipath._cli._runtime._contracts import UiPathRuntimeFactory
|
|
|
10
10
|
from uipath._cli._utils._console import ConsoleLogger
|
|
11
11
|
from uipath._cli.middlewares import MiddlewareResult
|
|
12
12
|
|
|
13
|
+
from .._tracing import _instrument_traceable_attributes
|
|
13
14
|
from ._runtime._context import LangGraphRuntimeContext
|
|
14
15
|
from ._runtime._runtime import LangGraphRuntime
|
|
15
16
|
|
|
@@ -24,6 +25,8 @@ def langgraph_dev_middleware(interface: Optional[str]) -> MiddlewareResult:
|
|
|
24
25
|
runtime_factory = UiPathRuntimeFactory(
|
|
25
26
|
LangGraphRuntime, LangGraphRuntimeContext
|
|
26
27
|
)
|
|
28
|
+
|
|
29
|
+
_instrument_traceable_attributes()
|
|
27
30
|
runtime_factory.add_instrumentor(LangChainInstrumentor, get_current_span)
|
|
28
31
|
app = UiPathDevTerminal(runtime_factory)
|
|
29
32
|
asyncio.run(app.run_async())
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from os import environ as env
|
|
3
2
|
from typing import List, Optional
|
|
4
3
|
|
|
4
|
+
from openinference.instrumentation.langchain import (
|
|
5
|
+
LangChainInstrumentor,
|
|
6
|
+
get_current_span,
|
|
7
|
+
)
|
|
5
8
|
from uipath._cli._evals._runtime import UiPathEvalContext, UiPathEvalRuntime
|
|
6
9
|
from uipath._cli._runtime._contracts import (
|
|
7
10
|
UiPathRuntimeFactory,
|
|
@@ -13,15 +16,15 @@ from uipath.eval._helpers import auto_discover_entrypoint
|
|
|
13
16
|
from uipath_langchain._cli._runtime._context import LangGraphRuntimeContext
|
|
14
17
|
from uipath_langchain._cli._runtime._runtime import LangGraphRuntime
|
|
15
18
|
from uipath_langchain._cli._utils._graph import LangGraphConfig
|
|
19
|
+
from uipath_langchain._tracing import (
|
|
20
|
+
LangChainExporter,
|
|
21
|
+
_instrument_traceable_attributes,
|
|
22
|
+
)
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
def langgraph_eval_middleware(
|
|
19
26
|
entrypoint: Optional[str], eval_set: Optional[str], eval_ids: List[str], **kwargs
|
|
20
27
|
) -> MiddlewareResult:
|
|
21
|
-
# Add default env variables
|
|
22
|
-
env["UIPATH_REQUESTING_PRODUCT"] = "uipath-python-sdk"
|
|
23
|
-
env["UIPATH_REQUESTING_FEATURE"] = "langgraph-agent"
|
|
24
|
-
|
|
25
28
|
config = LangGraphConfig()
|
|
26
29
|
if not config.exists:
|
|
27
30
|
return MiddlewareResult(
|
|
@@ -33,6 +36,8 @@ def langgraph_eval_middleware(
|
|
|
33
36
|
eval_context.eval_ids = eval_ids
|
|
34
37
|
|
|
35
38
|
try:
|
|
39
|
+
_instrument_traceable_attributes()
|
|
40
|
+
|
|
36
41
|
runtime_entrypoint = entrypoint or auto_discover_entrypoint()
|
|
37
42
|
|
|
38
43
|
def generate_runtime_context(
|
|
@@ -53,6 +58,11 @@ def langgraph_eval_middleware(
|
|
|
53
58
|
),
|
|
54
59
|
)
|
|
55
60
|
|
|
61
|
+
if eval_context.job_id:
|
|
62
|
+
runtime_factory.add_span_exporter(LangChainExporter())
|
|
63
|
+
|
|
64
|
+
runtime_factory.add_instrumentor(LangChainInstrumentor, get_current_span)
|
|
65
|
+
|
|
56
66
|
async def execute():
|
|
57
67
|
async with UiPathEvalRuntime.from_eval_context(
|
|
58
68
|
factory=runtime_factory, context=eval_context
|
uipath_langchain/_cli/cli_run.py
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
|
-
from os import environ as env
|
|
4
3
|
from typing import Optional
|
|
5
4
|
|
|
5
|
+
from openinference.instrumentation.langchain import (
|
|
6
|
+
LangChainInstrumentor,
|
|
7
|
+
get_current_span,
|
|
8
|
+
)
|
|
9
|
+
from uipath._cli._runtime._contracts import (
|
|
10
|
+
UiPathRuntimeFactory,
|
|
11
|
+
)
|
|
6
12
|
from uipath._cli.middlewares import MiddlewareResult
|
|
7
13
|
|
|
14
|
+
from .._tracing import LangChainExporter, _instrument_traceable_attributes
|
|
8
15
|
from ._runtime._exception import LangGraphRuntimeError
|
|
9
16
|
from ._runtime._runtime import ( # type: ignore[attr-defined]
|
|
10
17
|
LangGraphRuntime,
|
|
@@ -24,23 +31,35 @@ def langgraph_run_middleware(
|
|
|
24
31
|
) # Continue with normal flow if no langgraph.json
|
|
25
32
|
|
|
26
33
|
try:
|
|
27
|
-
# Add default env variables
|
|
28
|
-
env["UIPATH_REQUESTING_PRODUCT"] = "uipath-python-sdk"
|
|
29
|
-
env["UIPATH_REQUESTING_FEATURE"] = "langgraph-agent"
|
|
30
|
-
|
|
31
34
|
context = LangGraphRuntimeContext.with_defaults(**kwargs)
|
|
32
35
|
context.langgraph_config = config
|
|
33
36
|
context.entrypoint = entrypoint
|
|
34
37
|
context.input = input
|
|
35
38
|
context.resume = resume
|
|
36
39
|
|
|
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
|
+
|
|
37
50
|
async def execute():
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
44
63
|
|
|
45
64
|
asyncio.run(execute())
|
|
46
65
|
|
|
@@ -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,0 +1,222 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from opentelemetry.sdk.trace.export import (
|
|
6
|
+
SpanExportResult,
|
|
7
|
+
)
|
|
8
|
+
from uipath.tracing import LlmOpsHttpExporter
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _safe_parse_json(s: Any) -> Any:
|
|
14
|
+
"""Safely parse a JSON string, returning the original if not a string or on error."""
|
|
15
|
+
if not isinstance(s, str):
|
|
16
|
+
return s
|
|
17
|
+
try:
|
|
18
|
+
return json.loads(s)
|
|
19
|
+
except (json.JSONDecodeError, TypeError):
|
|
20
|
+
return s
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_llm_messages(attributes: Dict[str, Any], prefix: str) -> List[Dict[str, Any]]:
|
|
24
|
+
"""Extracts and reconstructs LLM messages from flattened attributes."""
|
|
25
|
+
messages: dict[int, dict[str, Any]] = {}
|
|
26
|
+
message_prefix = f"{prefix}."
|
|
27
|
+
|
|
28
|
+
for key, value in attributes.items():
|
|
29
|
+
if key.startswith(message_prefix):
|
|
30
|
+
parts = key[len(message_prefix) :].split(".")
|
|
31
|
+
if len(parts) >= 2 and parts[0].isdigit():
|
|
32
|
+
index = int(parts[0])
|
|
33
|
+
if index not in messages:
|
|
34
|
+
messages[index] = {}
|
|
35
|
+
current: Any = messages[index]
|
|
36
|
+
|
|
37
|
+
for i, part in enumerate(parts[1:-1]):
|
|
38
|
+
key_part: str | int = part
|
|
39
|
+
if part.isdigit() and (
|
|
40
|
+
i + 2 < len(parts) and parts[i + 2].isdigit()
|
|
41
|
+
):
|
|
42
|
+
key_part = int(part)
|
|
43
|
+
|
|
44
|
+
if isinstance(current, dict):
|
|
45
|
+
if key_part not in current:
|
|
46
|
+
current[key_part] = {}
|
|
47
|
+
current = current[key_part]
|
|
48
|
+
elif isinstance(current, list) and isinstance(key_part, int):
|
|
49
|
+
if key_part >= len(current):
|
|
50
|
+
current.append({})
|
|
51
|
+
current = current[key_part]
|
|
52
|
+
|
|
53
|
+
current[parts[-1]] = value
|
|
54
|
+
|
|
55
|
+
# Convert dict to list, ordered by index
|
|
56
|
+
return [messages[i] for i in sorted(messages.keys())]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class LangChainExporter(LlmOpsHttpExporter):
|
|
60
|
+
# Mapping of old attribute names to new attribute names or (new name, function)
|
|
61
|
+
ATTRIBUTE_MAPPING: dict[str, str | tuple[str, Any]] = {
|
|
62
|
+
"input.value": ("input", _safe_parse_json),
|
|
63
|
+
"output.value": ("output", _safe_parse_json),
|
|
64
|
+
"llm.model_name": "model",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Mapping of span types
|
|
68
|
+
SPAN_TYPE_MAPPING: dict[str, str] = {
|
|
69
|
+
"LLM": "completion",
|
|
70
|
+
"TOOL": "toolCall",
|
|
71
|
+
# Add more mappings as needed
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
75
|
+
super().__init__(*args, **kwargs)
|
|
76
|
+
|
|
77
|
+
def _map_llm_call_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
|
|
78
|
+
"""Maps attributes for LLM calls, handling flattened keys."""
|
|
79
|
+
result = attributes.copy() # Keep original attributes including basic mappings
|
|
80
|
+
|
|
81
|
+
# Token Usage
|
|
82
|
+
token_keys = {
|
|
83
|
+
"llm.token_count.prompt": "promptTokens",
|
|
84
|
+
"llm.token_count.completion": "completionTokens",
|
|
85
|
+
"llm.token_count.total": "totalTokens",
|
|
86
|
+
}
|
|
87
|
+
usage = {
|
|
88
|
+
new_key: attributes.get(old_key)
|
|
89
|
+
for old_key, new_key in token_keys.items()
|
|
90
|
+
if old_key in attributes
|
|
91
|
+
}
|
|
92
|
+
if usage:
|
|
93
|
+
result["usage"] = usage
|
|
94
|
+
|
|
95
|
+
# Input/Output Messages
|
|
96
|
+
result["input"] = _get_llm_messages(attributes, "llm.input_messages")
|
|
97
|
+
output_messages = _get_llm_messages(attributes, "llm.output_messages")
|
|
98
|
+
result["output"] = output_messages
|
|
99
|
+
|
|
100
|
+
# Invocation Parameters
|
|
101
|
+
invocation_params = _safe_parse_json(
|
|
102
|
+
attributes.get("llm.invocation_parameters", "{}")
|
|
103
|
+
)
|
|
104
|
+
if isinstance(invocation_params, dict):
|
|
105
|
+
result["model"] = invocation_params.get("model", result.get("model"))
|
|
106
|
+
settings: dict[str, Any] = {}
|
|
107
|
+
if "max_tokens" in invocation_params:
|
|
108
|
+
settings["maxTokens"] = invocation_params["max_tokens"]
|
|
109
|
+
if "temperature" in invocation_params:
|
|
110
|
+
settings["temperature"] = invocation_params["temperature"]
|
|
111
|
+
if settings:
|
|
112
|
+
result["settings"] = settings
|
|
113
|
+
|
|
114
|
+
# Tool Calls
|
|
115
|
+
tool_calls: list[dict[str, Any]] = []
|
|
116
|
+
for msg in output_messages:
|
|
117
|
+
# Ensure msg is a dictionary before proceeding
|
|
118
|
+
if not isinstance(msg, dict):
|
|
119
|
+
continue
|
|
120
|
+
msg_tool_calls = msg.get("message", {}).get("tool_calls", [])
|
|
121
|
+
|
|
122
|
+
# Ensure msg_tool_calls is a list
|
|
123
|
+
if not isinstance(msg_tool_calls, list):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
for tc in msg_tool_calls:
|
|
127
|
+
if not isinstance(tc, dict):
|
|
128
|
+
continue
|
|
129
|
+
tool_call_data = tc.get("tool_call", {})
|
|
130
|
+
if not isinstance(tool_call_data, dict):
|
|
131
|
+
continue
|
|
132
|
+
tool_calls.append(
|
|
133
|
+
{
|
|
134
|
+
"id": tool_call_data.get("id"),
|
|
135
|
+
"name": tool_call_data.get("function", {}).get("name"),
|
|
136
|
+
"arguments": _safe_parse_json(
|
|
137
|
+
tool_call_data.get("function", {}).get("arguments", "{}")
|
|
138
|
+
),
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
if tool_calls:
|
|
142
|
+
result["toolCalls"] = tool_calls
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
def _map_tool_call_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
|
|
147
|
+
"""Maps attributes for tool calls."""
|
|
148
|
+
result = attributes.copy() # Keep original attributes
|
|
149
|
+
|
|
150
|
+
result["type"] = "toolCall"
|
|
151
|
+
result["callId"] = attributes.get("call_id") or attributes.get("id")
|
|
152
|
+
result["toolName"] = attributes.get("tool.name")
|
|
153
|
+
result["arguments"] = _safe_parse_json(
|
|
154
|
+
attributes.get("input", attributes.get("input.value", "{}"))
|
|
155
|
+
)
|
|
156
|
+
result["toolType"] = "Integration"
|
|
157
|
+
result["result"] = _safe_parse_json(
|
|
158
|
+
attributes.get("output", attributes.get("output.value"))
|
|
159
|
+
)
|
|
160
|
+
result["error"] = None
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def _process_span_attributes(self, span_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
165
|
+
"""Extracts, transforms, and maps attributes for a span."""
|
|
166
|
+
if "Attributes" not in span_data:
|
|
167
|
+
return span_data
|
|
168
|
+
|
|
169
|
+
logger.info(f"Processing span: {span_data}")
|
|
170
|
+
|
|
171
|
+
attributes_val = span_data["Attributes"]
|
|
172
|
+
if isinstance(attributes_val, str):
|
|
173
|
+
try:
|
|
174
|
+
attributes: Dict[str, Any] = json.loads(attributes_val)
|
|
175
|
+
except json.JSONDecodeError as e:
|
|
176
|
+
logger.warning(f"Failed to parse attributes JSON: {e}")
|
|
177
|
+
return span_data
|
|
178
|
+
elif isinstance(attributes_val, dict):
|
|
179
|
+
attributes = attributes_val
|
|
180
|
+
else:
|
|
181
|
+
return span_data
|
|
182
|
+
|
|
183
|
+
# Determine SpanType
|
|
184
|
+
if "openinference.span.kind" in attributes:
|
|
185
|
+
span_type = attributes["openinference.span.kind"]
|
|
186
|
+
span_data["SpanType"] = self.SPAN_TYPE_MAPPING.get(span_type, span_type)
|
|
187
|
+
|
|
188
|
+
# Apply basic attribute mapping
|
|
189
|
+
for old_key, mapping in self.ATTRIBUTE_MAPPING.items():
|
|
190
|
+
if old_key in attributes:
|
|
191
|
+
if isinstance(mapping, tuple):
|
|
192
|
+
new_key, func = mapping
|
|
193
|
+
attributes[new_key] = func(attributes[old_key])
|
|
194
|
+
else:
|
|
195
|
+
new_key = mapping
|
|
196
|
+
attributes[new_key] = attributes[old_key]
|
|
197
|
+
|
|
198
|
+
# Apply detailed mapping based on SpanType
|
|
199
|
+
span_type = span_data.get("SpanType")
|
|
200
|
+
if span_type == "completion":
|
|
201
|
+
processed_attributes = self._map_llm_call_attributes(attributes)
|
|
202
|
+
elif span_type == "toolCall":
|
|
203
|
+
processed_attributes = self._map_tool_call_attributes(attributes)
|
|
204
|
+
else:
|
|
205
|
+
processed_attributes = attributes.copy()
|
|
206
|
+
|
|
207
|
+
span_data["Attributes"] = json.dumps(processed_attributes)
|
|
208
|
+
|
|
209
|
+
logger.info(f"Transformed span: {span_data}")
|
|
210
|
+
return span_data
|
|
211
|
+
|
|
212
|
+
def _send_with_retries(
|
|
213
|
+
self, url: str, payload: List[Dict[str, Any]], max_retries: int = 4
|
|
214
|
+
) -> SpanExportResult:
|
|
215
|
+
# Transform attributes in each span's payload before sending
|
|
216
|
+
transformed_payload = [self._process_span_attributes(span) for span in payload]
|
|
217
|
+
|
|
218
|
+
return super()._send_with_retries(
|
|
219
|
+
url=url,
|
|
220
|
+
payload=transformed_payload,
|
|
221
|
+
max_retries=max_retries,
|
|
222
|
+
)
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import logging
|
|
3
|
-
from zoneinfo import ZoneInfo
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
class IgnoreSpecificUrl(logging.Filter):
|
|
@@ -28,25 +26,3 @@ def _setup_tracer_httpx_logging(url: str):
|
|
|
28
26
|
# Create a custom logger for httpx
|
|
29
27
|
# Add the custom filter to the root logger
|
|
30
28
|
logging.getLogger("httpx").addFilter(IgnoreSpecificUrl(url))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _simple_serialize_defaults(obj):
|
|
34
|
-
if hasattr(obj, "model_dump"):
|
|
35
|
-
return obj.model_dump(exclude_none=True, mode="json")
|
|
36
|
-
if hasattr(obj, "dict"):
|
|
37
|
-
return obj.dict()
|
|
38
|
-
if hasattr(obj, "to_dict"):
|
|
39
|
-
return obj.to_dict()
|
|
40
|
-
|
|
41
|
-
if isinstance(obj, (set, tuple)):
|
|
42
|
-
if hasattr(obj, "_asdict") and callable(obj._asdict):
|
|
43
|
-
return obj._asdict()
|
|
44
|
-
return list(obj)
|
|
45
|
-
|
|
46
|
-
if isinstance(obj, datetime.datetime):
|
|
47
|
-
return obj.isoformat()
|
|
48
|
-
|
|
49
|
-
if isinstance(obj, (datetime.timezone, ZoneInfo)):
|
|
50
|
-
return obj.tzname(None)
|
|
51
|
-
|
|
52
|
-
return str(obj)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uipath-langchain
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.132
|
|
4
4
|
Summary: UiPath Langchain
|
|
5
5
|
Project-URL: Homepage, https://uipath.com
|
|
6
6
|
Project-URL: Repository, https://github.com/UiPath/uipath-langchain-python
|
|
@@ -25,7 +25,7 @@ Requires-Dist: openai>=1.65.5
|
|
|
25
25
|
Requires-Dist: openinference-instrumentation-langchain>=0.1.50
|
|
26
26
|
Requires-Dist: pydantic-settings>=2.6.0
|
|
27
27
|
Requires-Dist: python-dotenv>=1.0.1
|
|
28
|
-
Requires-Dist: uipath<2.2.0,>=2.1.
|
|
28
|
+
Requires-Dist: uipath<2.2.0,>=2.1.56
|
|
29
29
|
Provides-Extra: langchain
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|