uipath-langchain 0.0.131__py3-none-any.whl → 0.0.133__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.
- 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/tools/__init__.py +0 -0
- uipath_langchain/tools/preconfigured.py +191 -0
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.133.dist-info}/METADATA +3 -2
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.133.dist-info}/RECORD +17 -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.133.dist-info}/WHEEL +0 -0
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.133.dist-info}/entry_points.txt +0 -0
- {uipath_langchain-0.0.131.dist-info → uipath_langchain-0.0.133.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Iterable, Optional, Type
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from jsonschema_pydantic import jsonschema_to_pydantic as create_model # type: ignore
|
|
7
|
+
from langchain_core.caches import BaseCache
|
|
8
|
+
from langchain_core.runnables.utils import Output
|
|
9
|
+
from langchain_core.tools import BaseTool, StructuredTool
|
|
10
|
+
from langgraph.types import interrupt
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from uipath import UiPath
|
|
13
|
+
from uipath.agent.models.agent import (
|
|
14
|
+
AgentDefinition,
|
|
15
|
+
AgentEscalationChannel,
|
|
16
|
+
AgentEscalationResourceConfig,
|
|
17
|
+
AgentIntegrationToolResourceConfig,
|
|
18
|
+
AgentProcessToolResourceConfig,
|
|
19
|
+
AgentResourceConfig,
|
|
20
|
+
)
|
|
21
|
+
from uipath.models import CreateAction, InvokeProcess
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_process_tool(resource: AgentProcessToolResourceConfig) -> Iterable[BaseTool]:
|
|
27
|
+
async def process(**kwargs) -> BaseModel:
|
|
28
|
+
return interrupt(
|
|
29
|
+
InvokeProcess(
|
|
30
|
+
name=resource.name,
|
|
31
|
+
input_arguments=kwargs,
|
|
32
|
+
process_folder_path=resource.properties.folder_path,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
input_schema = create_model(resource.input_schema)
|
|
37
|
+
|
|
38
|
+
class ProcessTool(StructuredTool):
|
|
39
|
+
@property
|
|
40
|
+
def OutputType(self) -> type[Output]:
|
|
41
|
+
return create_model(resource.output_schema)
|
|
42
|
+
|
|
43
|
+
yield ProcessTool(
|
|
44
|
+
name=resource.name,
|
|
45
|
+
args_schema=input_schema,
|
|
46
|
+
description=resource.description,
|
|
47
|
+
coroutine=process,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_escalation_tool_from_channel(channel: AgentEscalationChannel) -> BaseTool:
|
|
52
|
+
async def escalate(**kwargs) -> BaseModel:
|
|
53
|
+
recipients = channel.recipients
|
|
54
|
+
if len(recipients) > 1:
|
|
55
|
+
logger.warning(
|
|
56
|
+
"Received more than one recipient. Defaulting to first recipient."
|
|
57
|
+
)
|
|
58
|
+
assignee = recipients[0].value if recipients else None
|
|
59
|
+
return interrupt(
|
|
60
|
+
CreateAction(
|
|
61
|
+
title=channel.description,
|
|
62
|
+
data=kwargs,
|
|
63
|
+
assignee=assignee,
|
|
64
|
+
app_name=channel.properties.app_name,
|
|
65
|
+
app_folder_path=None, # Channels specify folder name but not folder path.
|
|
66
|
+
app_folder_key=channel.properties.resource_key,
|
|
67
|
+
app_key=channel.properties.resource_key,
|
|
68
|
+
app_version=channel.properties.app_version,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
input_schema = create_model(channel.input_schema)
|
|
73
|
+
|
|
74
|
+
class EscalationTool(StructuredTool):
|
|
75
|
+
@property
|
|
76
|
+
def OutputType(self) -> type[Output]:
|
|
77
|
+
return create_model(channel.output_schema)
|
|
78
|
+
|
|
79
|
+
return EscalationTool(
|
|
80
|
+
name=channel.name,
|
|
81
|
+
args_schema=input_schema,
|
|
82
|
+
description=channel.description,
|
|
83
|
+
coroutine=escalate,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def create_escalation_tool(
|
|
88
|
+
resource: AgentEscalationResourceConfig,
|
|
89
|
+
) -> Iterable[BaseTool]:
|
|
90
|
+
for channel in resource.channels:
|
|
91
|
+
yield create_escalation_tool_from_channel(channel)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_integration_tool(
|
|
95
|
+
resource: AgentIntegrationToolResourceConfig,
|
|
96
|
+
) -> Iterable[BaseTool]:
|
|
97
|
+
async def integration(**kwargs) -> BaseModel:
|
|
98
|
+
uipath = UiPath()
|
|
99
|
+
remote_connection = await uipath.connections.retrieve_async(
|
|
100
|
+
resource.properties.connection.id
|
|
101
|
+
)
|
|
102
|
+
token = await uipath.connections.retrieve_token_async(
|
|
103
|
+
resource.properties.connection.id
|
|
104
|
+
)
|
|
105
|
+
tool_url = f"{remote_connection.api_base_uri}/v3/element/instances/{remote_connection.element_instance_id}{resource.properties.tool_path}"
|
|
106
|
+
|
|
107
|
+
response = await httpx.AsyncClient().request(
|
|
108
|
+
resource.properties.method,
|
|
109
|
+
tool_url,
|
|
110
|
+
headers={"Authorization": f"Bearer {token.access_token}"},
|
|
111
|
+
content=json.dumps(kwargs),
|
|
112
|
+
)
|
|
113
|
+
return response.json()
|
|
114
|
+
|
|
115
|
+
input_schema = create_model(resource.input_schema)
|
|
116
|
+
|
|
117
|
+
class IntegrationTool(StructuredTool):
|
|
118
|
+
@property
|
|
119
|
+
def OutputType(self) -> type[Output]:
|
|
120
|
+
return create_model({})
|
|
121
|
+
|
|
122
|
+
yield IntegrationTool(
|
|
123
|
+
name=resource.name,
|
|
124
|
+
args_schema=input_schema,
|
|
125
|
+
description=resource.description,
|
|
126
|
+
coroutine=integration,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_cached_wrapper_from_tool(
|
|
131
|
+
wrapped: BaseTool, cache: Optional[BaseCache]
|
|
132
|
+
) -> BaseTool:
|
|
133
|
+
if cache is None:
|
|
134
|
+
return wrapped
|
|
135
|
+
else:
|
|
136
|
+
|
|
137
|
+
async def cached_invocation(**kwargs) -> BaseModel:
|
|
138
|
+
namespace = f"{wrapped.name}.tool_invoke"
|
|
139
|
+
key = str(kwargs)
|
|
140
|
+
cached = cache.lookup(namespace, key)
|
|
141
|
+
if cached:
|
|
142
|
+
return cached[0]
|
|
143
|
+
response = await wrapped.ainvoke(input=kwargs)
|
|
144
|
+
cache.update(namespace, key, [response])
|
|
145
|
+
return response
|
|
146
|
+
|
|
147
|
+
input_schema = wrapped.args_schema
|
|
148
|
+
|
|
149
|
+
class CachedTool(StructuredTool):
|
|
150
|
+
OutputType: Type[BaseModel] = wrapped.OutputType
|
|
151
|
+
|
|
152
|
+
return CachedTool(
|
|
153
|
+
name=wrapped.name,
|
|
154
|
+
args_schema=input_schema,
|
|
155
|
+
description=wrapped.description,
|
|
156
|
+
coroutine=cached_invocation,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def create_cached_wrapper(
|
|
161
|
+
tools: Iterable[BaseTool], cache: Optional[BaseCache]
|
|
162
|
+
) -> Iterable[BaseTool]:
|
|
163
|
+
for wrapped in tools:
|
|
164
|
+
yield create_cached_wrapper_from_tool(wrapped, cache)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def create_resource_tool(
|
|
168
|
+
resource: AgentResourceConfig, cache: Optional[BaseCache] = None
|
|
169
|
+
) -> Iterable[BaseTool]:
|
|
170
|
+
match resource:
|
|
171
|
+
case AgentProcessToolResourceConfig():
|
|
172
|
+
return create_cached_wrapper(create_process_tool(resource), cache)
|
|
173
|
+
case AgentIntegrationToolResourceConfig():
|
|
174
|
+
return create_cached_wrapper(create_integration_tool(resource), cache)
|
|
175
|
+
case AgentEscalationResourceConfig():
|
|
176
|
+
return create_cached_wrapper(create_escalation_tool(resource), cache)
|
|
177
|
+
case _:
|
|
178
|
+
raise NotImplementedError()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def safe_extract_tools(
|
|
182
|
+
agent_definition: AgentDefinition, cache: Optional[BaseCache] = None
|
|
183
|
+
) -> list[BaseTool]:
|
|
184
|
+
tools = []
|
|
185
|
+
for resource in agent_definition.resources:
|
|
186
|
+
try:
|
|
187
|
+
for structured_tool in create_resource_tool(resource, cache):
|
|
188
|
+
tools.append(structured_tool)
|
|
189
|
+
except NotImplementedError:
|
|
190
|
+
logger.warning(f"Unable to convert {resource.name} into a tool.")
|
|
191
|
+
return tools
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: uipath-langchain
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.133
|
|
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
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Topic :: Software Development :: Build Tools
|
|
16
16
|
Requires-Python: >=3.10
|
|
17
17
|
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: jsonschema-pydantic>=0.6
|
|
18
19
|
Requires-Dist: langchain-community>=0.3.21
|
|
19
20
|
Requires-Dist: langchain-core>=0.3.34
|
|
20
21
|
Requires-Dist: langchain-openai>=0.3.3
|
|
@@ -25,7 +26,7 @@ Requires-Dist: openai>=1.65.5
|
|
|
25
26
|
Requires-Dist: openinference-instrumentation-langchain>=0.1.50
|
|
26
27
|
Requires-Dist: pydantic-settings>=2.6.0
|
|
27
28
|
Requires-Dist: python-dotenv>=1.0.1
|
|
28
|
-
Requires-Dist: uipath<2.2.0,>=2.1.
|
|
29
|
+
Requires-Dist: uipath<2.2.0,>=2.1.60
|
|
29
30
|
Provides-Extra: langchain
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
uipath_langchain/__init__.py,sha256=VBrvQn7d3nuOdN7zEnV2_S-uhmkjgEIlXiFVeZxZakQ,80
|
|
2
2
|
uipath_langchain/middlewares.py,sha256=6ljfbtWekrYc5G9KWDLSaViJ1DVIaNM-4qeB1BfHywE,731
|
|
3
3
|
uipath_langchain/_cli/__init__.py,sha256=juqd9PbXs4yg45zMJ7BHAOPQjb7sgEbWE9InBtGZhfo,24
|
|
4
|
-
uipath_langchain/_cli/cli_dev.py,sha256=
|
|
5
|
-
uipath_langchain/_cli/cli_eval.py,sha256=
|
|
4
|
+
uipath_langchain/_cli/cli_dev.py,sha256=3e9RldNGirIk9184NdLK6kDuGeeqZjekTxbSZRtXjBE,1505
|
|
5
|
+
uipath_langchain/_cli/cli_eval.py,sha256=mvaKRF-pvxdNa2mk_ude9SYSd_kmMBo-hChlXokXOcw,2795
|
|
6
6
|
uipath_langchain/_cli/cli_init.py,sha256=xhxJ8tuMSrVUNHvltgyPpOrvgMA-wq9shHeYYwvHILs,8199
|
|
7
7
|
uipath_langchain/_cli/cli_new.py,sha256=KKLxCzz7cDQ__rRr_a496IHWlSQXhmrBNgmKHnXAnTY,2336
|
|
8
|
-
uipath_langchain/_cli/cli_run.py,sha256=
|
|
8
|
+
uipath_langchain/_cli/cli_run.py,sha256=hRcoXJgOIFceCswzTfZKyzqVee3j-oSh-13EfuQmmE8,2614
|
|
9
9
|
uipath_langchain/_cli/_runtime/_context.py,sha256=yyzYJDmk2fkH4T5gm4cLGRyXtjLESrpzHBT9euqluTA,817
|
|
10
10
|
uipath_langchain/_cli/_runtime/_conversation.py,sha256=S1KTx_q-La7ikPRT3nBcIp8t-J9CF0QB0DCduQIIB28,11149
|
|
11
11
|
uipath_langchain/_cli/_runtime/_exception.py,sha256=USKkLYkG-dzjX3fEiMMOHnVUpiXJs_xF0OQXCCOvbYM,546
|
|
12
|
-
uipath_langchain/_cli/_runtime/_input.py,sha256=
|
|
12
|
+
uipath_langchain/_cli/_runtime/_input.py,sha256=Zx-8ZEr5Z796gdd3NnrlNObMIuXJobAV9mZwOql67Lo,5658
|
|
13
13
|
uipath_langchain/_cli/_runtime/_output.py,sha256=yJOZPWv2FRUJWv1NRs9JmpB4QMTDXu8jrxoaKrfJvzw,9078
|
|
14
|
-
uipath_langchain/_cli/_runtime/_runtime.py,sha256=
|
|
14
|
+
uipath_langchain/_cli/_runtime/_runtime.py,sha256=cVt0TlsEBmKv6EcFTtCOT9ITsguzLDqFGMdhPOiBPRc,14318
|
|
15
15
|
uipath_langchain/_cli/_templates/langgraph.json.template,sha256=eeh391Gta_hoRgaNaZ58nW1LNvCVXA7hlAH6l7Veous,107
|
|
16
16
|
uipath_langchain/_cli/_templates/main.py.template,sha256=GpSblGH2hwS9ibqQmX2iB2nsmOA5zDfEEF4ChLiMxbQ,875
|
|
17
17
|
uipath_langchain/_cli/_utils/_graph.py,sha256=nMJWy8FmaD9rqPUY2lHc5uVpUzbXD1RO12uJnhe0kdo,6803
|
|
18
|
-
uipath_langchain/
|
|
18
|
+
uipath_langchain/_tracing/__init__.py,sha256=UqrLc_WimpzKY82M0LJsgJ-HFQUQFjOmOlD1XQ8V-R4,181
|
|
19
|
+
uipath_langchain/_tracing/_instrument_traceable.py,sha256=8f9FyAKWE6kH1N8ErbpwqZHAzNjGwbLjQn7jdX5yAgA,4343
|
|
20
|
+
uipath_langchain/_tracing/_oteladapter.py,sha256=bhbLLTBwZ1rLDL-BgbvB8oR11y1Tp8jEw2mrAuVJ5go,8433
|
|
21
|
+
uipath_langchain/_tracing/_utils.py,sha256=r_fiSk3HDDAcePY_UbbEYiSbNqzn5gFeMPYBDvGrFx0,902
|
|
22
|
+
uipath_langchain/_utils/__init__.py,sha256=-w-4TD9ZnJDCpj4VIPXhJciukrmDJJbmnOFnhAkAaEU,81
|
|
19
23
|
uipath_langchain/_utils/_request_mixin.py,sha256=ddKFs_0mjoFCmvPTiOTPJh1IIqYUo5CUka-B7zAZphE,19695
|
|
20
24
|
uipath_langchain/_utils/_settings.py,sha256=2fExMQJ88YptfldmzMfZIpsx-m1gfMkeYGf5t6KIe0A,3084
|
|
21
25
|
uipath_langchain/_utils/_sleep_policy.py,sha256=e9pHdjmcCj4CVoFM1jMyZFelH11YatsgWfpyrfXzKBQ,1251
|
|
@@ -26,15 +30,12 @@ uipath_langchain/embeddings/__init__.py,sha256=QICtYB58ZyqFfDQrEaO8lTEgAU5NuEKlR
|
|
|
26
30
|
uipath_langchain/embeddings/embeddings.py,sha256=45gKyb6HVKigwE-0CXeZcAk33c0mteaEdPGa8hviqcw,4339
|
|
27
31
|
uipath_langchain/retrievers/__init__.py,sha256=rOn7PyyHgZ4pMnXWPkGqmuBmx8eGuo-Oyndo7Wm9IUU,108
|
|
28
32
|
uipath_langchain/retrievers/context_grounding_retriever.py,sha256=YLCIwy89LhLnNqcM0YJ5mZoeNyCs5UiKD3Wly8gnW1E,2239
|
|
29
|
-
uipath_langchain/
|
|
30
|
-
uipath_langchain/
|
|
31
|
-
uipath_langchain/tracers/_events.py,sha256=CJri76SSdu7rGJIkXurJ2C5sQahfSK4E5UWwWYdEAtE,922
|
|
32
|
-
uipath_langchain/tracers/_instrument_traceable.py,sha256=0e841zVzcPWjOGtmBx0GeHbq3JoqsmWv6gVPzDOKNTM,13496
|
|
33
|
-
uipath_langchain/tracers/_utils.py,sha256=JOT1tKMdvqjMDtj2WbmbOWMeMlTXBWavxWpogX7KlRA,1543
|
|
33
|
+
uipath_langchain/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
uipath_langchain/tools/preconfigured.py,sha256=Ucl9mta_su66j-5uOC-vZzyc3V2-Em6q9WO1qsmQv48,6291
|
|
34
35
|
uipath_langchain/vectorstores/__init__.py,sha256=w8qs1P548ud1aIcVA_QhBgf_jZDrRMK5Lono78yA8cs,114
|
|
35
36
|
uipath_langchain/vectorstores/context_grounding_vectorstore.py,sha256=TncIXG-YsUlO0R5ZYzWsM-Dj1SVCZbzmo2LraVxXelc,9559
|
|
36
|
-
uipath_langchain-0.0.
|
|
37
|
-
uipath_langchain-0.0.
|
|
38
|
-
uipath_langchain-0.0.
|
|
39
|
-
uipath_langchain-0.0.
|
|
40
|
-
uipath_langchain-0.0.
|
|
37
|
+
uipath_langchain-0.0.133.dist-info/METADATA,sha256=zcSdl4I5iJsBi0jOf7IUUprBv39KfQZyrM1EzVH1OCQ,4275
|
|
38
|
+
uipath_langchain-0.0.133.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
39
|
+
uipath_langchain-0.0.133.dist-info/entry_points.txt,sha256=FUtzqGOEntlJKMJIXhQUfT7ZTbQmGhke1iCmDWZaQZI,81
|
|
40
|
+
uipath_langchain-0.0.133.dist-info/licenses/LICENSE,sha256=JDpt-uotAkHFmxpwxi6gwx6HQ25e-lG4U_Gzcvgp7JY,1063
|
|
41
|
+
uipath_langchain-0.0.133.dist-info/RECORD,,
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import logging
|
|
4
|
-
import queue
|
|
5
|
-
import uuid
|
|
6
|
-
import warnings
|
|
7
|
-
from os import environ as env
|
|
8
|
-
from typing import Any, Dict, Optional
|
|
9
|
-
|
|
10
|
-
import httpx
|
|
11
|
-
from langchain_core.tracers.base import AsyncBaseTracer
|
|
12
|
-
from langchain_core.tracers.schemas import Run
|
|
13
|
-
from pydantic import PydanticDeprecationWarning
|
|
14
|
-
from uipath._cli._runtime._contracts import UiPathTraceContext
|
|
15
|
-
|
|
16
|
-
from ._events import CustomTraceEvents, FunctionCallEventData
|
|
17
|
-
from ._utils import _setup_tracer_httpx_logging, _simple_serialize_defaults
|
|
18
|
-
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
|
-
_setup_tracer_httpx_logging("/llmops_/api/Agent/trace/")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class Status:
|
|
25
|
-
SUCCESS = 1
|
|
26
|
-
ERROR = 2
|
|
27
|
-
INTERRUPTED = 3
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class AsyncUiPathTracer(AsyncBaseTracer):
|
|
31
|
-
def __init__(
|
|
32
|
-
self,
|
|
33
|
-
context: Optional[UiPathTraceContext] = None,
|
|
34
|
-
client: Optional[httpx.AsyncClient] = None,
|
|
35
|
-
**kwargs,
|
|
36
|
-
):
|
|
37
|
-
super().__init__(**kwargs)
|
|
38
|
-
|
|
39
|
-
self.client = client or httpx.AsyncClient()
|
|
40
|
-
self.retries = 3
|
|
41
|
-
self.log_queue: queue.Queue[dict[str, Any]] = queue.Queue()
|
|
42
|
-
|
|
43
|
-
self.context = context or UiPathTraceContext()
|
|
44
|
-
|
|
45
|
-
self.base_url = self._get_base_url()
|
|
46
|
-
|
|
47
|
-
auth_token = env.get("UNATTENDED_USER_ACCESS_TOKEN") or env.get(
|
|
48
|
-
"UIPATH_ACCESS_TOKEN"
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
self.headers = {"Authorization": f"Bearer {auth_token}"}
|
|
52
|
-
|
|
53
|
-
self.running = True
|
|
54
|
-
self.worker_task = asyncio.create_task(self._worker())
|
|
55
|
-
self.function_call_run_map: Dict[str, Run] = {}
|
|
56
|
-
|
|
57
|
-
async def on_custom_event(
|
|
58
|
-
self,
|
|
59
|
-
name: str,
|
|
60
|
-
data: Any,
|
|
61
|
-
*,
|
|
62
|
-
run_id: uuid.UUID,
|
|
63
|
-
tags=None,
|
|
64
|
-
metadata=None,
|
|
65
|
-
**kwargs: Any,
|
|
66
|
-
) -> None:
|
|
67
|
-
if name == CustomTraceEvents.UIPATH_TRACE_FUNCTION_CALL:
|
|
68
|
-
# only handle the function call event
|
|
69
|
-
|
|
70
|
-
if not isinstance(data, FunctionCallEventData):
|
|
71
|
-
logger.warning(
|
|
72
|
-
f"Received unexpected data type for function call event: {type(data)}"
|
|
73
|
-
)
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
if data.event_type == "call":
|
|
77
|
-
run = self.run_map[str(run_id)]
|
|
78
|
-
child_run = run.create_child(
|
|
79
|
-
name=data.function_name,
|
|
80
|
-
run_type=data.run_type,
|
|
81
|
-
tags=data.tags,
|
|
82
|
-
inputs=data.inputs,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
if data.metadata is not None:
|
|
86
|
-
run.add_metadata(data.metadata)
|
|
87
|
-
|
|
88
|
-
call_uuid = data.call_uuid
|
|
89
|
-
self.function_call_run_map[call_uuid] = child_run
|
|
90
|
-
|
|
91
|
-
self._send_span(run)
|
|
92
|
-
|
|
93
|
-
if data.event_type == "completion":
|
|
94
|
-
call_uuid = data.call_uuid
|
|
95
|
-
previous_run = self.function_call_run_map.pop(call_uuid, None)
|
|
96
|
-
|
|
97
|
-
if previous_run:
|
|
98
|
-
previous_run.end(
|
|
99
|
-
outputs=self._safe_dict_dump(data.output), error=data.error
|
|
100
|
-
)
|
|
101
|
-
self._send_span(previous_run)
|
|
102
|
-
|
|
103
|
-
async def wait_for_all_tracers(self) -> None:
|
|
104
|
-
"""
|
|
105
|
-
Wait for all pending log requests to complete
|
|
106
|
-
"""
|
|
107
|
-
self.running = False
|
|
108
|
-
if self.worker_task:
|
|
109
|
-
await self.worker_task
|
|
110
|
-
|
|
111
|
-
async def _worker(self):
|
|
112
|
-
"""Worker loop that processes logs from the queue."""
|
|
113
|
-
while self.running:
|
|
114
|
-
try:
|
|
115
|
-
if self.log_queue.empty():
|
|
116
|
-
await asyncio.sleep(1)
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
span_data = self.log_queue.get_nowait()
|
|
120
|
-
|
|
121
|
-
url = self._build_url(self.context.trace_id)
|
|
122
|
-
|
|
123
|
-
for attempt in range(self.retries):
|
|
124
|
-
response = await self.client.post(
|
|
125
|
-
url,
|
|
126
|
-
headers=self.headers,
|
|
127
|
-
json=[span_data], # api expects a list of spans
|
|
128
|
-
timeout=10,
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
if response.is_success:
|
|
132
|
-
break
|
|
133
|
-
|
|
134
|
-
await asyncio.sleep(0.5 * (2**attempt)) # Exponential backoff
|
|
135
|
-
|
|
136
|
-
if 400 <= response.status_code < 600:
|
|
137
|
-
logger.warning(
|
|
138
|
-
f"Error when sending trace: {response}. Body is: {response.text}"
|
|
139
|
-
)
|
|
140
|
-
except Exception as e:
|
|
141
|
-
logger.warning(f"Exception when sending trace: {e}", exc_info=e)
|
|
142
|
-
|
|
143
|
-
# wait for a bit to ensure all logs are sent
|
|
144
|
-
await asyncio.sleep(1)
|
|
145
|
-
|
|
146
|
-
# try to send any remaining logs in the queue
|
|
147
|
-
while True:
|
|
148
|
-
try:
|
|
149
|
-
if self.log_queue.empty():
|
|
150
|
-
break
|
|
151
|
-
|
|
152
|
-
span_data = self.log_queue.get_nowait()
|
|
153
|
-
url = self._build_url(self.context.trace_id)
|
|
154
|
-
|
|
155
|
-
response = await self.client.post(
|
|
156
|
-
url,
|
|
157
|
-
headers=self.headers,
|
|
158
|
-
json=[span_data], # api expects a list of spans
|
|
159
|
-
timeout=10,
|
|
160
|
-
)
|
|
161
|
-
except Exception as e:
|
|
162
|
-
logger.warning(f"Exception when sending trace: {e}", exc_info=e)
|
|
163
|
-
|
|
164
|
-
async def _persist_run(self, run: Run) -> None:
|
|
165
|
-
# Determine if this is a start or end trace based on whether end_time is set
|
|
166
|
-
self._send_span(run)
|
|
167
|
-
|
|
168
|
-
def _send_span(self, run: Run) -> None:
|
|
169
|
-
"""Send span data for a run to the API"""
|
|
170
|
-
run_id = str(run.id)
|
|
171
|
-
|
|
172
|
-
try:
|
|
173
|
-
start_time = (
|
|
174
|
-
run.start_time.isoformat() if run.start_time is not None else None
|
|
175
|
-
)
|
|
176
|
-
end_time = (
|
|
177
|
-
run.end_time.isoformat() if run.end_time is not None else start_time
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
parent_id = (
|
|
181
|
-
str(run.parent_run_id)
|
|
182
|
-
if run.parent_run_id is not None
|
|
183
|
-
else self.context.parent_span_id
|
|
184
|
-
)
|
|
185
|
-
attributes = self._safe_jsons_dump(self._run_to_dict(run))
|
|
186
|
-
status = self._determine_status(run.error)
|
|
187
|
-
|
|
188
|
-
span_data = {
|
|
189
|
-
"id": run_id,
|
|
190
|
-
"parentId": parent_id,
|
|
191
|
-
"traceId": self.context.trace_id,
|
|
192
|
-
"name": run.name,
|
|
193
|
-
"startTime": start_time,
|
|
194
|
-
"endTime": end_time,
|
|
195
|
-
"referenceId": self.context.reference_id,
|
|
196
|
-
"attributes": attributes,
|
|
197
|
-
"organizationId": self.context.org_id,
|
|
198
|
-
"tenantId": self.context.tenant_id,
|
|
199
|
-
"spanType": "LangGraphRun",
|
|
200
|
-
"status": status,
|
|
201
|
-
"jobKey": self.context.job_id,
|
|
202
|
-
"folderKey": self.context.folder_key,
|
|
203
|
-
"processKey": self.context.folder_key,
|
|
204
|
-
"expiryTimeUtc": None,
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
self.log_queue.put(span_data)
|
|
208
|
-
except Exception as e:
|
|
209
|
-
logger.warning(f"Exception when adding trace to queue: {e}.")
|
|
210
|
-
|
|
211
|
-
async def _start_trace(self, run: Run) -> None:
|
|
212
|
-
await super()._start_trace(run)
|
|
213
|
-
await self._persist_run(run)
|
|
214
|
-
|
|
215
|
-
async def _end_trace(self, run: Run) -> None:
|
|
216
|
-
await super()._end_trace(run)
|
|
217
|
-
await self._persist_run(run)
|
|
218
|
-
|
|
219
|
-
def _determine_status(self, error: Optional[str]):
|
|
220
|
-
if error:
|
|
221
|
-
if error.startswith("GraphInterrupt("):
|
|
222
|
-
return Status.INTERRUPTED
|
|
223
|
-
|
|
224
|
-
return Status.ERROR
|
|
225
|
-
|
|
226
|
-
return Status.SUCCESS
|
|
227
|
-
|
|
228
|
-
def _safe_jsons_dump(self, obj) -> str:
|
|
229
|
-
try:
|
|
230
|
-
json_str = json.dumps(obj, default=_simple_serialize_defaults)
|
|
231
|
-
return json_str
|
|
232
|
-
except Exception as e:
|
|
233
|
-
logger.warning(f"Error serializing object to JSON: {e}")
|
|
234
|
-
return "{ }"
|
|
235
|
-
|
|
236
|
-
def _safe_dict_dump(self, obj) -> Dict[str, Any]:
|
|
237
|
-
try:
|
|
238
|
-
serialized = json.loads(json.dumps(obj, default=_simple_serialize_defaults))
|
|
239
|
-
return serialized
|
|
240
|
-
except Exception as e:
|
|
241
|
-
# Last resort - string representation
|
|
242
|
-
logger.warning(f"Error serializing object to JSON: {e}")
|
|
243
|
-
return {"raw": str(obj)}
|
|
244
|
-
|
|
245
|
-
def _run_to_dict(self, run: Run):
|
|
246
|
-
with warnings.catch_warnings():
|
|
247
|
-
warnings.simplefilter("ignore", category=PydanticDeprecationWarning)
|
|
248
|
-
|
|
249
|
-
# Helper function to safely copy values
|
|
250
|
-
def safe_copy(value):
|
|
251
|
-
if value is None:
|
|
252
|
-
return None
|
|
253
|
-
if hasattr(value, "copy") and callable(value.copy):
|
|
254
|
-
return value.copy()
|
|
255
|
-
return value
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
**run.dict(exclude={"child_runs", "inputs", "outputs", "serialized"}),
|
|
259
|
-
"inputs": safe_copy(run.inputs),
|
|
260
|
-
"outputs": safe_copy(run.outputs),
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
def _get_base_url(self) -> str:
|
|
264
|
-
uipath_url = (
|
|
265
|
-
env.get("UIPATH_URL") or "https://cloud.uipath.com/dummyOrg/dummyTennant/"
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
uipath_url = uipath_url.rstrip("/")
|
|
269
|
-
|
|
270
|
-
return uipath_url
|
|
271
|
-
|
|
272
|
-
def _build_url(self, trace_id: Optional[str]) -> str:
|
|
273
|
-
"""Construct the URL for the API request."""
|
|
274
|
-
return f"{self.base_url}/llmopstenant_/api/Traces/spans?traceId={trace_id}&source=Robots"
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Literal, Optional
|
|
2
|
-
|
|
3
|
-
RUN_TYPE_T = Literal[
|
|
4
|
-
"tool", "chain", "llm", "retriever", "embedding", "prompt", "parser"
|
|
5
|
-
]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CustomTraceEvents:
|
|
9
|
-
UIPATH_TRACE_FUNCTION_CALL = "__uipath_trace_function_call"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class FunctionCallEventData:
|
|
13
|
-
def __init__(
|
|
14
|
-
self,
|
|
15
|
-
function_name: str,
|
|
16
|
-
event_type: str,
|
|
17
|
-
inputs: Dict[str, Any],
|
|
18
|
-
call_uuid: str,
|
|
19
|
-
output: Any,
|
|
20
|
-
error: Optional[str] = None,
|
|
21
|
-
run_type: Optional[RUN_TYPE_T] = None,
|
|
22
|
-
tags: Optional[List[str]] = None,
|
|
23
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
24
|
-
):
|
|
25
|
-
self.function_name = function_name
|
|
26
|
-
self.event_type = event_type
|
|
27
|
-
self.inputs = inputs
|
|
28
|
-
self.call_uuid = call_uuid
|
|
29
|
-
self.output = output
|
|
30
|
-
self.error = error
|
|
31
|
-
self.run_type = run_type or "chain"
|
|
32
|
-
self.tags = tags
|
|
33
|
-
self.metadata = metadata
|