uipath-langchain 0.1.28__py3-none-any.whl → 0.3.1__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/_templates/langgraph.json.template +2 -4
- uipath_langchain/_cli/cli_new.py +1 -2
- uipath_langchain/_utils/_request_mixin.py +8 -0
- uipath_langchain/_utils/_settings.py +3 -2
- uipath_langchain/agent/guardrails/__init__.py +0 -16
- uipath_langchain/agent/guardrails/actions/__init__.py +2 -0
- uipath_langchain/agent/guardrails/actions/block_action.py +1 -1
- uipath_langchain/agent/guardrails/actions/escalate_action.py +265 -138
- uipath_langchain/agent/guardrails/actions/filter_action.py +290 -0
- uipath_langchain/agent/guardrails/actions/log_action.py +1 -1
- uipath_langchain/agent/guardrails/guardrail_nodes.py +193 -42
- uipath_langchain/agent/guardrails/guardrails_factory.py +235 -14
- uipath_langchain/agent/guardrails/types.py +0 -12
- uipath_langchain/agent/guardrails/utils.py +177 -0
- uipath_langchain/agent/react/agent.py +24 -9
- uipath_langchain/agent/react/constants.py +1 -2
- uipath_langchain/agent/react/file_type_handler.py +123 -0
- uipath_langchain/agent/{guardrails → react/guardrails}/guardrails_subgraph.py +119 -25
- uipath_langchain/agent/react/init_node.py +16 -1
- uipath_langchain/agent/react/job_attachments.py +125 -0
- uipath_langchain/agent/react/json_utils.py +183 -0
- uipath_langchain/agent/react/jsonschema_pydantic_converter.py +76 -0
- uipath_langchain/agent/react/llm_node.py +41 -10
- uipath_langchain/agent/react/llm_with_files.py +76 -0
- uipath_langchain/agent/react/router.py +48 -37
- uipath_langchain/agent/react/types.py +19 -1
- uipath_langchain/agent/react/utils.py +30 -4
- uipath_langchain/agent/tools/__init__.py +7 -1
- uipath_langchain/agent/tools/context_tool.py +151 -1
- uipath_langchain/agent/tools/escalation_tool.py +46 -15
- uipath_langchain/agent/tools/integration_tool.py +20 -16
- uipath_langchain/agent/tools/internal_tools/__init__.py +5 -0
- uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py +113 -0
- uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py +54 -0
- uipath_langchain/agent/tools/mcp_tool.py +86 -0
- uipath_langchain/agent/tools/process_tool.py +8 -1
- uipath_langchain/agent/tools/static_args.py +18 -40
- uipath_langchain/agent/tools/tool_factory.py +13 -5
- uipath_langchain/agent/tools/tool_node.py +133 -4
- uipath_langchain/agent/tools/utils.py +31 -0
- uipath_langchain/agent/wrappers/__init__.py +6 -0
- uipath_langchain/agent/wrappers/job_attachment_wrapper.py +62 -0
- uipath_langchain/agent/wrappers/static_args_wrapper.py +34 -0
- uipath_langchain/chat/__init__.py +4 -0
- uipath_langchain/chat/bedrock.py +16 -0
- uipath_langchain/chat/mapper.py +60 -42
- uipath_langchain/chat/openai.py +56 -26
- uipath_langchain/chat/supported_models.py +9 -0
- uipath_langchain/chat/vertex.py +62 -46
- uipath_langchain/embeddings/embeddings.py +18 -12
- uipath_langchain/runtime/factory.py +10 -5
- uipath_langchain/runtime/runtime.py +38 -35
- uipath_langchain/runtime/schema.py +72 -16
- uipath_langchain/runtime/storage.py +178 -71
- {uipath_langchain-0.1.28.dist-info → uipath_langchain-0.3.1.dist-info}/METADATA +7 -4
- uipath_langchain-0.3.1.dist-info/RECORD +90 -0
- uipath_langchain-0.1.28.dist-info/RECORD +0 -76
- {uipath_langchain-0.1.28.dist-info → uipath_langchain-0.3.1.dist-info}/WHEEL +0 -0
- {uipath_langchain-0.1.28.dist-info → uipath_langchain-0.3.1.dist-info}/entry_points.txt +0 -0
- {uipath_langchain-0.1.28.dist-info → uipath_langchain-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from jsonschema_pydantic_converter import transform as create_model
|
|
7
|
-
from langchain.tools import ToolRuntime
|
|
8
6
|
from langchain_core.messages import ToolMessage
|
|
9
|
-
from langchain_core.
|
|
7
|
+
from langchain_core.messages.tool import ToolCall
|
|
8
|
+
from langchain_core.tools import BaseTool, StructuredTool
|
|
10
9
|
from langgraph.types import Command, interrupt
|
|
11
10
|
from uipath.agent.models.agent import (
|
|
12
11
|
AgentEscalationChannel,
|
|
@@ -16,7 +15,10 @@ from uipath.agent.models.agent import (
|
|
|
16
15
|
from uipath.eval.mocks import mockable
|
|
17
16
|
from uipath.platform.common import CreateEscalation
|
|
18
17
|
|
|
19
|
-
from
|
|
18
|
+
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
|
|
19
|
+
|
|
20
|
+
from ..react.types import AgentGraphNode, AgentGraphState, AgentTerminationSource
|
|
21
|
+
from .tool_node import ToolWrapperMixin
|
|
20
22
|
from .utils import sanitize_tool_name
|
|
21
23
|
|
|
22
24
|
|
|
@@ -27,7 +29,11 @@ class EscalationAction(str, Enum):
|
|
|
27
29
|
END = "end"
|
|
28
30
|
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
class StructuredToolWithWrapper(StructuredTool, ToolWrapperMixin):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_escalation_tool(resource: AgentEscalationResourceConfig) -> BaseTool:
|
|
31
37
|
"""Uses interrupt() for Action Center human-in-the-loop."""
|
|
32
38
|
|
|
33
39
|
tool_name: str = f"escalate_{sanitize_tool_name(resource.name)}"
|
|
@@ -48,10 +54,9 @@ def create_escalation_tool(resource: AgentEscalationResourceConfig) -> Structure
|
|
|
48
54
|
description=resource.description,
|
|
49
55
|
input_schema=input_model.model_json_schema(),
|
|
50
56
|
output_schema=output_model.model_json_schema(),
|
|
57
|
+
example_calls=channel.properties.example_calls,
|
|
51
58
|
)
|
|
52
|
-
async def escalation_tool_fn(
|
|
53
|
-
runtime: ToolRuntime, **kwargs: Any
|
|
54
|
-
) -> Command[Any] | Any:
|
|
59
|
+
async def escalation_tool_fn(**kwargs: Any) -> dict[str, Any]:
|
|
55
60
|
task_title = channel.task_title or "Escalation Task"
|
|
56
61
|
|
|
57
62
|
result = interrupt(
|
|
@@ -72,22 +77,41 @@ def create_escalation_tool(resource: AgentEscalationResourceConfig) -> Structure
|
|
|
72
77
|
escalation_action = getattr(result, "action", None)
|
|
73
78
|
escalation_output = getattr(result, "data", {})
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
outcome_str = (
|
|
76
81
|
channel.outcome_mapping.get(escalation_action)
|
|
77
82
|
if channel.outcome_mapping and escalation_action
|
|
78
83
|
else None
|
|
79
84
|
)
|
|
85
|
+
outcome = (
|
|
86
|
+
EscalationAction(outcome_str) if outcome_str else EscalationAction.CONTINUE
|
|
87
|
+
)
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
return {
|
|
90
|
+
"action": outcome,
|
|
91
|
+
"output": escalation_output,
|
|
92
|
+
"escalation_action": escalation_action,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async def escalation_wrapper(
|
|
96
|
+
tool: BaseTool,
|
|
97
|
+
call: ToolCall,
|
|
98
|
+
state: AgentGraphState,
|
|
99
|
+
) -> dict[str, Any] | Command[Any]:
|
|
100
|
+
result = await tool.ainvoke(call["args"])
|
|
101
|
+
|
|
102
|
+
if result["action"] == EscalationAction.END:
|
|
103
|
+
output_detail = f"Escalation output: {result['output']}"
|
|
104
|
+
termination_title = (
|
|
105
|
+
f"Agent run ended based on escalation outcome {result['action']} "
|
|
106
|
+
f"with directive {result['escalation_action']}"
|
|
107
|
+
)
|
|
84
108
|
|
|
85
109
|
return Command(
|
|
86
110
|
update={
|
|
87
111
|
"messages": [
|
|
88
112
|
ToolMessage(
|
|
89
113
|
content=f"{termination_title}. {output_detail}",
|
|
90
|
-
tool_call_id=
|
|
114
|
+
tool_call_id=call["id"],
|
|
91
115
|
)
|
|
92
116
|
],
|
|
93
117
|
"termination": {
|
|
@@ -99,13 +123,20 @@ def create_escalation_tool(resource: AgentEscalationResourceConfig) -> Structure
|
|
|
99
123
|
goto=AgentGraphNode.TERMINATE,
|
|
100
124
|
)
|
|
101
125
|
|
|
102
|
-
return
|
|
126
|
+
return result["output"]
|
|
103
127
|
|
|
104
|
-
tool =
|
|
128
|
+
tool = StructuredToolWithWrapper(
|
|
105
129
|
name=tool_name,
|
|
106
130
|
description=resource.description,
|
|
107
131
|
args_schema=input_model,
|
|
108
132
|
coroutine=escalation_tool_fn,
|
|
133
|
+
metadata={
|
|
134
|
+
"tool_type": "escalation",
|
|
135
|
+
"display_name": channel.properties.app_name,
|
|
136
|
+
"channel_type": channel.type,
|
|
137
|
+
"assignee": assignee,
|
|
138
|
+
},
|
|
109
139
|
)
|
|
140
|
+
tool.set_tool_wrappers(awrapper=escalation_wrapper)
|
|
110
141
|
|
|
111
142
|
return tool
|
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
import copy
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from jsonschema_pydantic_converter import transform as create_model
|
|
7
|
-
from langchain.tools import ToolRuntime
|
|
8
6
|
from langchain_core.tools import StructuredTool
|
|
9
7
|
from uipath.agent.models.agent import AgentIntegrationToolResourceConfig
|
|
10
8
|
from uipath.eval.mocks import mockable
|
|
11
9
|
from uipath.platform import UiPath
|
|
12
10
|
from uipath.platform.connections import ActivityMetadata, ActivityParameterLocationInfo
|
|
13
11
|
|
|
14
|
-
from .
|
|
12
|
+
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
|
|
13
|
+
from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin
|
|
14
|
+
|
|
15
15
|
from .structured_tool_with_output_type import StructuredToolWithOutputType
|
|
16
|
-
from .utils import sanitize_tool_name
|
|
16
|
+
from .utils import sanitize_dict_for_serialization, sanitize_tool_name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StructuredToolWithStaticArgs(StructuredToolWithOutputType, ToolWrapperMixin):
|
|
20
|
+
pass
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def remove_asterisk_from_properties(fields: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -149,33 +153,33 @@ def create_integration_tool(
|
|
|
149
153
|
description=resource.description,
|
|
150
154
|
input_schema=input_model.model_json_schema(),
|
|
151
155
|
output_schema=output_model.model_json_schema(),
|
|
156
|
+
example_calls=resource.properties.example_calls,
|
|
152
157
|
)
|
|
153
|
-
async def integration_tool_fn(
|
|
158
|
+
async def integration_tool_fn(**kwargs: Any):
|
|
154
159
|
try:
|
|
155
|
-
# we manually validating here and not passing input_model to StructuredTool
|
|
156
|
-
# because langchain itself will block their own injected arguments (like runtime) if the model is strict
|
|
157
|
-
val_args = input_model.model_validate(kwargs)
|
|
158
|
-
args = handle_static_args(
|
|
159
|
-
resource=resource,
|
|
160
|
-
runtime=runtime,
|
|
161
|
-
input_args=val_args.model_dump(),
|
|
162
|
-
)
|
|
163
160
|
result = await sdk.connections.invoke_activity_async(
|
|
164
161
|
activity_metadata=activity_metadata,
|
|
165
162
|
connection_id=connection_id,
|
|
166
|
-
activity_input=
|
|
163
|
+
activity_input=sanitize_dict_for_serialization(kwargs),
|
|
167
164
|
)
|
|
168
165
|
except Exception:
|
|
169
166
|
raise
|
|
170
167
|
|
|
171
168
|
return result
|
|
172
169
|
|
|
173
|
-
|
|
170
|
+
from uipath_langchain.agent.wrappers.static_args_wrapper import (
|
|
171
|
+
get_static_args_wrapper,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
wrapper = get_static_args_wrapper(resource)
|
|
175
|
+
|
|
176
|
+
tool = StructuredToolWithStaticArgs(
|
|
174
177
|
name=tool_name,
|
|
175
178
|
description=resource.description,
|
|
176
|
-
args_schema=
|
|
179
|
+
args_schema=input_model,
|
|
177
180
|
coroutine=integration_tool_fn,
|
|
178
181
|
output_type=output_model,
|
|
179
182
|
)
|
|
183
|
+
tool.set_tool_wrappers(awrapper=wrapper)
|
|
180
184
|
|
|
181
185
|
return tool
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from langchain_core.language_models import BaseChatModel
|
|
5
|
+
from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
|
|
6
|
+
from langchain_core.tools import StructuredTool
|
|
7
|
+
from uipath.agent.models.agent import (
|
|
8
|
+
AgentInternalToolResourceConfig,
|
|
9
|
+
)
|
|
10
|
+
from uipath.eval.mocks import mockable
|
|
11
|
+
from uipath.platform import UiPath
|
|
12
|
+
|
|
13
|
+
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
|
|
14
|
+
from uipath_langchain.agent.react.llm_with_files import FileInfo, llm_call_with_files
|
|
15
|
+
from uipath_langchain.agent.tools.structured_tool_with_output_type import (
|
|
16
|
+
StructuredToolWithOutputType,
|
|
17
|
+
)
|
|
18
|
+
from uipath_langchain.agent.tools.tool_node import ToolWrapperMixin
|
|
19
|
+
from uipath_langchain.agent.tools.utils import sanitize_tool_name
|
|
20
|
+
|
|
21
|
+
ANALYZE_FILES_SYSTEM_MESSAGE = (
|
|
22
|
+
"Process the provided files to complete the given task. "
|
|
23
|
+
"Analyze the files contents thoroughly to deliver an accurate response "
|
|
24
|
+
"based on the extracted information."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AnalyzeFileTool(StructuredToolWithOutputType, ToolWrapperMixin):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_analyze_file_tool(
|
|
33
|
+
resource: AgentInternalToolResourceConfig, llm: BaseChatModel
|
|
34
|
+
) -> StructuredTool:
|
|
35
|
+
from uipath_langchain.agent.wrappers.job_attachment_wrapper import (
|
|
36
|
+
get_job_attachment_wrapper,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
tool_name = sanitize_tool_name(resource.name)
|
|
40
|
+
input_model = create_model(resource.input_schema)
|
|
41
|
+
output_model = create_model(resource.output_schema)
|
|
42
|
+
|
|
43
|
+
@mockable(
|
|
44
|
+
name=resource.name,
|
|
45
|
+
description=resource.description,
|
|
46
|
+
input_schema=input_model.model_json_schema(),
|
|
47
|
+
output_schema=output_model.model_json_schema(),
|
|
48
|
+
)
|
|
49
|
+
async def tool_fn(**kwargs: Any):
|
|
50
|
+
if "analysisTask" not in kwargs:
|
|
51
|
+
raise ValueError("Argument 'analysisTask' is not available")
|
|
52
|
+
if "attachments" not in kwargs:
|
|
53
|
+
raise ValueError("Argument 'attachments' is not available")
|
|
54
|
+
|
|
55
|
+
attachments = kwargs["attachments"]
|
|
56
|
+
analysisTask = kwargs["analysisTask"]
|
|
57
|
+
|
|
58
|
+
files = await _resolve_job_attachment_arguments(attachments)
|
|
59
|
+
messages: list[AnyMessage] = [
|
|
60
|
+
SystemMessage(content=ANALYZE_FILES_SYSTEM_MESSAGE),
|
|
61
|
+
HumanMessage(content=analysisTask),
|
|
62
|
+
]
|
|
63
|
+
result = await llm_call_with_files(messages, files, llm)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
wrapper = get_job_attachment_wrapper()
|
|
67
|
+
tool = AnalyzeFileTool(
|
|
68
|
+
name=tool_name,
|
|
69
|
+
description=resource.description,
|
|
70
|
+
args_schema=input_model,
|
|
71
|
+
coroutine=tool_fn,
|
|
72
|
+
output_type=output_model,
|
|
73
|
+
)
|
|
74
|
+
tool.set_tool_wrappers(awrapper=wrapper)
|
|
75
|
+
return tool
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def _resolve_job_attachment_arguments(
|
|
79
|
+
attachments: list[Any],
|
|
80
|
+
) -> list[FileInfo]:
|
|
81
|
+
"""Resolve job attachments to FileInfo objects.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
attachments: List of job attachment objects (dynamically typed from schema)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of FileInfo objects with blob URIs for each attachment
|
|
88
|
+
"""
|
|
89
|
+
client = UiPath()
|
|
90
|
+
file_infos: list[FileInfo] = []
|
|
91
|
+
|
|
92
|
+
for attachment in attachments:
|
|
93
|
+
# Access using Pydantic field aliases (ID, FullName, MimeType)
|
|
94
|
+
# These are dynamically created from the JSON schema
|
|
95
|
+
attachment_id_value = getattr(attachment, "ID", None)
|
|
96
|
+
if attachment_id_value is None:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
attachment_id = uuid.UUID(attachment_id_value)
|
|
100
|
+
mime_type = getattr(attachment, "MimeType", "")
|
|
101
|
+
|
|
102
|
+
blob_info = await client.attachments.get_blob_file_access_uri_async(
|
|
103
|
+
key=attachment_id
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
file_info = FileInfo(
|
|
107
|
+
url=blob_info.uri,
|
|
108
|
+
name=blob_info.name,
|
|
109
|
+
mime_type=mime_type,
|
|
110
|
+
)
|
|
111
|
+
file_infos.append(file_info)
|
|
112
|
+
|
|
113
|
+
return file_infos
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Factory for creating internal agent tools.
|
|
2
|
+
|
|
3
|
+
This module provides a factory pattern for creating internal tools used by agents.
|
|
4
|
+
Internal tools are built-in tools that provide core functionality for agents, such as
|
|
5
|
+
file analysis, data processing, or other utilities that don't require external integrations.
|
|
6
|
+
|
|
7
|
+
Supported Internal Tools:
|
|
8
|
+
- ANALYZE_FILES: Tool for analyzing file contents and extracting information
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from uipath.agent.models.agent import AgentInternalToolResourceConfig
|
|
12
|
+
>>> resource = AgentInternalToolResourceConfig(...)
|
|
13
|
+
>>> tool = create_internal_tool(resource)
|
|
14
|
+
>>> # Use the tool in your agent workflow
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Callable
|
|
18
|
+
|
|
19
|
+
from langchain_core.language_models import BaseChatModel
|
|
20
|
+
from langchain_core.tools import StructuredTool
|
|
21
|
+
from uipath.agent.models.agent import (
|
|
22
|
+
AgentInternalToolResourceConfig,
|
|
23
|
+
AgentInternalToolType,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .analyze_files_tool import create_analyze_file_tool
|
|
27
|
+
|
|
28
|
+
_INTERNAL_TOOL_HANDLERS: dict[
|
|
29
|
+
AgentInternalToolType,
|
|
30
|
+
Callable[[AgentInternalToolResourceConfig, BaseChatModel], StructuredTool],
|
|
31
|
+
] = {
|
|
32
|
+
AgentInternalToolType.ANALYZE_FILES: create_analyze_file_tool,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_internal_tool(
|
|
37
|
+
resource: AgentInternalToolResourceConfig, llm: BaseChatModel
|
|
38
|
+
) -> StructuredTool:
|
|
39
|
+
"""Create an internal tool based on the resource configuration.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If the tool type is not supported (no handler exists for it).
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
tool_type = resource.properties.tool_type
|
|
46
|
+
|
|
47
|
+
handler = _INTERNAL_TOOL_HANDLERS.get(tool_type)
|
|
48
|
+
if handler is None:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Unsupported internal tool type: {tool_type}. "
|
|
51
|
+
f"Supported types: {list[AgentInternalToolType](_INTERNAL_TOOL_HANDLERS.keys())}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return handler(resource, llm)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from collections import Counter, defaultdict
|
|
4
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
5
|
+
from itertools import chain
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from langchain_core.tools import BaseTool
|
|
9
|
+
from langchain_mcp_adapters.tools import load_mcp_tools
|
|
10
|
+
from mcp import ClientSession
|
|
11
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
12
|
+
from uipath._utils._ssl_context import get_httpx_client_kwargs
|
|
13
|
+
from uipath.agent.models.agent import AgentMcpResourceConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _deduplicate_tools(tools: list[BaseTool]) -> list[BaseTool]:
|
|
17
|
+
"""Deduplicate tools by appending numeric suffix to duplicate names."""
|
|
18
|
+
counts = Counter(tool.name for tool in tools)
|
|
19
|
+
seen: defaultdict[str, int] = defaultdict(int)
|
|
20
|
+
|
|
21
|
+
for tool in tools:
|
|
22
|
+
if counts[tool.name] > 1:
|
|
23
|
+
seen[tool.name] += 1
|
|
24
|
+
tool.name = f"{tool.name}_{seen[tool.name]}"
|
|
25
|
+
|
|
26
|
+
return tools
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _filter_tools(tools: list[BaseTool], cfg: AgentMcpResourceConfig) -> list[BaseTool]:
|
|
30
|
+
"""Filter tools to only include those in available_tools."""
|
|
31
|
+
allowed = {t.name for t in cfg.available_tools}
|
|
32
|
+
return [t for t in tools if t.name in allowed]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def create_mcp_tools(
|
|
37
|
+
config: AgentMcpResourceConfig | list[AgentMcpResourceConfig],
|
|
38
|
+
max_concurrency: int = 5,
|
|
39
|
+
):
|
|
40
|
+
"""Connect to UiPath MCP server(s) and yield LangChain-compatible tools."""
|
|
41
|
+
if not (base_url := os.getenv("UIPATH_URL")):
|
|
42
|
+
raise ValueError("UIPATH_URL environment variable is not set")
|
|
43
|
+
if not (access_token := os.getenv("UIPATH_ACCESS_TOKEN")):
|
|
44
|
+
raise ValueError("UIPATH_ACCESS_TOKEN environment variable is not set")
|
|
45
|
+
|
|
46
|
+
configs = config if isinstance(config, list) else [config]
|
|
47
|
+
enabled = [c for c in configs if c.is_enabled is not False]
|
|
48
|
+
|
|
49
|
+
if not enabled:
|
|
50
|
+
yield []
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
base_url = base_url.rstrip("/")
|
|
54
|
+
semaphore = asyncio.Semaphore(max_concurrency)
|
|
55
|
+
|
|
56
|
+
default_client_kwargs = get_httpx_client_kwargs()
|
|
57
|
+
client_kwargs = {
|
|
58
|
+
**default_client_kwargs,
|
|
59
|
+
"headers": {"Authorization": f"Bearer {access_token}"},
|
|
60
|
+
"timeout": httpx.Timeout(60),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async def init_session(
|
|
64
|
+
session: ClientSession, cfg: AgentMcpResourceConfig
|
|
65
|
+
) -> list[BaseTool]:
|
|
66
|
+
async with semaphore:
|
|
67
|
+
await session.initialize()
|
|
68
|
+
tools = await load_mcp_tools(session)
|
|
69
|
+
return _filter_tools(tools, cfg)
|
|
70
|
+
|
|
71
|
+
async def create_session(
|
|
72
|
+
stack: AsyncExitStack, cfg: AgentMcpResourceConfig
|
|
73
|
+
) -> ClientSession:
|
|
74
|
+
url = f"{base_url}/agenthub_/mcp/{cfg.folder_path}/{cfg.slug}"
|
|
75
|
+
http_client = await stack.enter_async_context(
|
|
76
|
+
httpx.AsyncClient(**client_kwargs)
|
|
77
|
+
)
|
|
78
|
+
read, write, _ = await stack.enter_async_context(
|
|
79
|
+
streamable_http_client(url=url, http_client=http_client)
|
|
80
|
+
)
|
|
81
|
+
return await stack.enter_async_context(ClientSession(read, write))
|
|
82
|
+
|
|
83
|
+
async with AsyncExitStack() as stack:
|
|
84
|
+
sessions = [(await create_session(stack, cfg), cfg) for cfg in enabled]
|
|
85
|
+
results = await asyncio.gather(*[init_session(s, cfg) for s, cfg in sessions])
|
|
86
|
+
yield _deduplicate_tools(list(chain.from_iterable(results)))
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from jsonschema_pydantic_converter import transform as create_model
|
|
6
5
|
from langchain_core.tools import StructuredTool
|
|
7
6
|
from langgraph.types import interrupt
|
|
8
7
|
from uipath.agent.models.agent import AgentProcessToolResourceConfig
|
|
9
8
|
from uipath.eval.mocks import mockable
|
|
10
9
|
from uipath.platform.common import InvokeProcess
|
|
11
10
|
|
|
11
|
+
from uipath_langchain.agent.react.jsonschema_pydantic_converter import create_model
|
|
12
|
+
|
|
12
13
|
from .structured_tool_with_output_type import StructuredToolWithOutputType
|
|
13
14
|
from .utils import sanitize_tool_name
|
|
14
15
|
|
|
@@ -27,6 +28,7 @@ def create_process_tool(resource: AgentProcessToolResourceConfig) -> StructuredT
|
|
|
27
28
|
description=resource.description,
|
|
28
29
|
input_schema=input_model.model_json_schema(),
|
|
29
30
|
output_schema=output_model.model_json_schema(),
|
|
31
|
+
example_calls=resource.properties.example_calls,
|
|
30
32
|
)
|
|
31
33
|
async def process_tool_fn(**kwargs: Any):
|
|
32
34
|
return interrupt(
|
|
@@ -44,6 +46,11 @@ def create_process_tool(resource: AgentProcessToolResourceConfig) -> StructuredT
|
|
|
44
46
|
args_schema=input_model,
|
|
45
47
|
coroutine=process_tool_fn,
|
|
46
48
|
output_type=output_model,
|
|
49
|
+
metadata={
|
|
50
|
+
"tool_type": "process",
|
|
51
|
+
"display_name": process_name,
|
|
52
|
+
"folder_path": folder_path,
|
|
53
|
+
},
|
|
47
54
|
)
|
|
48
55
|
|
|
49
56
|
return tool
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
"""Handles static arguments for tool calls."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
|
|
5
5
|
from jsonpath_ng import parse # type: ignore[import-untyped]
|
|
6
|
-
from
|
|
6
|
+
from pydantic import BaseModel
|
|
7
7
|
from uipath.agent.models.agent import (
|
|
8
8
|
AgentIntegrationToolParameter,
|
|
9
9
|
AgentIntegrationToolResourceConfig,
|
|
10
10
|
BaseAgentResourceConfig,
|
|
11
11
|
)
|
|
12
12
|
|
|
13
|
+
from .utils import sanitize_dict_for_serialization
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def resolve_static_args(
|
|
15
17
|
resource: BaseAgentResourceConfig,
|
|
16
|
-
agent_input:
|
|
17
|
-
) ->
|
|
18
|
+
agent_input: dict[str, Any],
|
|
19
|
+
) -> dict[str, Any]:
|
|
18
20
|
"""Resolves static arguments for a given resource with a given input.
|
|
19
21
|
|
|
20
22
|
Args:
|
|
21
23
|
resource: The agent resource configuration.
|
|
22
|
-
input:
|
|
24
|
+
input: The input arguments passed to the agent.
|
|
23
25
|
|
|
24
26
|
Returns:
|
|
25
27
|
A dictionary of expanded arguments to be used in the tool call.
|
|
@@ -35,9 +37,9 @@ def resolve_static_args(
|
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def resolve_integration_static_args(
|
|
38
|
-
parameters:
|
|
39
|
-
agent_input:
|
|
40
|
-
) ->
|
|
40
|
+
parameters: list[AgentIntegrationToolParameter],
|
|
41
|
+
agent_input: dict[str, Any],
|
|
42
|
+
) -> dict[str, Any]:
|
|
41
43
|
"""Resolves static arguments for an integration tool resource.
|
|
42
44
|
|
|
43
45
|
Args:
|
|
@@ -48,7 +50,7 @@ def resolve_integration_static_args(
|
|
|
48
50
|
A dictionary of expanded static arguments for the integration tool.
|
|
49
51
|
"""
|
|
50
52
|
|
|
51
|
-
static_args:
|
|
53
|
+
static_args: dict[str, Any] = {}
|
|
52
54
|
for param in parameters:
|
|
53
55
|
value = None
|
|
54
56
|
|
|
@@ -75,34 +77,10 @@ def resolve_integration_static_args(
|
|
|
75
77
|
return static_args
|
|
76
78
|
|
|
77
79
|
|
|
78
|
-
def sanitize_for_serialization(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
79
|
-
"""Convert Pydantic models in args to dicts."""
|
|
80
|
-
converted_args: Dict[str, Any] = {}
|
|
81
|
-
for key, value in args.items():
|
|
82
|
-
# handle Pydantic model
|
|
83
|
-
if hasattr(value, "model_dump"):
|
|
84
|
-
converted_args[key] = value.model_dump()
|
|
85
|
-
|
|
86
|
-
elif isinstance(value, list):
|
|
87
|
-
# handle list of Pydantic models
|
|
88
|
-
converted_list = []
|
|
89
|
-
for item in value:
|
|
90
|
-
if hasattr(item, "model_dump"):
|
|
91
|
-
converted_list.append(item.model_dump())
|
|
92
|
-
else:
|
|
93
|
-
converted_list.append(item)
|
|
94
|
-
converted_args[key] = converted_list
|
|
95
|
-
|
|
96
|
-
# handle regular value or unexpected type
|
|
97
|
-
else:
|
|
98
|
-
converted_args[key] = value
|
|
99
|
-
return converted_args
|
|
100
|
-
|
|
101
|
-
|
|
102
80
|
def apply_static_args(
|
|
103
|
-
static_args:
|
|
104
|
-
kwargs:
|
|
105
|
-
) ->
|
|
81
|
+
static_args: dict[str, Any],
|
|
82
|
+
kwargs: dict[str, Any],
|
|
83
|
+
) -> dict[str, Any]:
|
|
106
84
|
"""Applies static arguments to the given input arguments.
|
|
107
85
|
|
|
108
86
|
Args:
|
|
@@ -113,7 +91,7 @@ def apply_static_args(
|
|
|
113
91
|
Merged input arguments with static arguments applied.
|
|
114
92
|
"""
|
|
115
93
|
|
|
116
|
-
sanitized_args =
|
|
94
|
+
sanitized_args = sanitize_dict_for_serialization(kwargs)
|
|
117
95
|
for json_path, value in static_args.items():
|
|
118
96
|
expr = parse(json_path)
|
|
119
97
|
expr.update_or_create(sanitized_args, value)
|
|
@@ -122,8 +100,8 @@ def apply_static_args(
|
|
|
122
100
|
|
|
123
101
|
|
|
124
102
|
def handle_static_args(
|
|
125
|
-
resource: BaseAgentResourceConfig,
|
|
126
|
-
) ->
|
|
103
|
+
resource: BaseAgentResourceConfig, state: BaseModel, input_args: dict[str, Any]
|
|
104
|
+
) -> dict[str, Any]:
|
|
127
105
|
"""Resolves and applies static arguments for a tool call.
|
|
128
106
|
Args:
|
|
129
107
|
resource: The agent resource configuration.
|
|
@@ -133,6 +111,6 @@ def handle_static_args(
|
|
|
133
111
|
A dictionary of input arguments with static arguments applied.
|
|
134
112
|
"""
|
|
135
113
|
|
|
136
|
-
static_args = resolve_static_args(resource, dict(
|
|
114
|
+
static_args = resolve_static_args(resource, dict(state))
|
|
137
115
|
merged_args = apply_static_args(static_args, input_args)
|
|
138
116
|
return merged_args
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Factory functions for creating tools from agent resources."""
|
|
2
2
|
|
|
3
|
-
from langchain_core.
|
|
3
|
+
from langchain_core.language_models import BaseChatModel
|
|
4
|
+
from langchain_core.tools import BaseTool
|
|
4
5
|
from uipath.agent.models.agent import (
|
|
5
6
|
AgentContextResourceConfig,
|
|
6
7
|
AgentEscalationResourceConfig,
|
|
7
8
|
AgentIntegrationToolResourceConfig,
|
|
9
|
+
AgentInternalToolResourceConfig,
|
|
8
10
|
AgentProcessToolResourceConfig,
|
|
9
11
|
BaseAgentResourceConfig,
|
|
10
12
|
LowCodeAgentDefinition,
|
|
@@ -13,14 +15,17 @@ from uipath.agent.models.agent import (
|
|
|
13
15
|
from .context_tool import create_context_tool
|
|
14
16
|
from .escalation_tool import create_escalation_tool
|
|
15
17
|
from .integration_tool import create_integration_tool
|
|
18
|
+
from .internal_tools import create_internal_tool
|
|
16
19
|
from .process_tool import create_process_tool
|
|
17
20
|
|
|
18
21
|
|
|
19
|
-
async def create_tools_from_resources(
|
|
22
|
+
async def create_tools_from_resources(
|
|
23
|
+
agent: LowCodeAgentDefinition, llm: BaseChatModel
|
|
24
|
+
) -> list[BaseTool]:
|
|
20
25
|
tools: list[BaseTool] = []
|
|
21
26
|
|
|
22
27
|
for resource in agent.resources:
|
|
23
|
-
tool = await _build_tool_for_resource(resource)
|
|
28
|
+
tool = await _build_tool_for_resource(resource, llm)
|
|
24
29
|
if tool is not None:
|
|
25
30
|
tools.append(tool)
|
|
26
31
|
|
|
@@ -28,8 +33,8 @@ async def create_tools_from_resources(agent: LowCodeAgentDefinition) -> list[Bas
|
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
async def _build_tool_for_resource(
|
|
31
|
-
resource: BaseAgentResourceConfig,
|
|
32
|
-
) ->
|
|
36
|
+
resource: BaseAgentResourceConfig, llm: BaseChatModel
|
|
37
|
+
) -> BaseTool | None:
|
|
33
38
|
if isinstance(resource, AgentProcessToolResourceConfig):
|
|
34
39
|
return create_process_tool(resource)
|
|
35
40
|
|
|
@@ -42,4 +47,7 @@ async def _build_tool_for_resource(
|
|
|
42
47
|
elif isinstance(resource, AgentIntegrationToolResourceConfig):
|
|
43
48
|
return create_integration_tool(resource)
|
|
44
49
|
|
|
50
|
+
elif isinstance(resource, AgentInternalToolResourceConfig):
|
|
51
|
+
return create_internal_tool(resource, llm)
|
|
52
|
+
|
|
45
53
|
return None
|