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
|
@@ -1,22 +1,151 @@
|
|
|
1
1
|
"""Tool node factory wiring directly to LangGraph's ToolNode."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
+
from inspect import signature
|
|
5
|
+
from typing import Any, Awaitable, Callable, Literal
|
|
4
6
|
|
|
7
|
+
from langchain_core.messages.ai import AIMessage
|
|
8
|
+
from langchain_core.messages.tool import ToolCall, ToolMessage
|
|
9
|
+
from langchain_core.runnables.config import RunnableConfig
|
|
5
10
|
from langchain_core.tools import BaseTool
|
|
6
|
-
from langgraph.
|
|
11
|
+
from langgraph._internal._runnable import RunnableCallable
|
|
12
|
+
from langgraph.types import Command
|
|
13
|
+
from pydantic import BaseModel
|
|
7
14
|
|
|
15
|
+
# the type safety can be improved with generics
|
|
16
|
+
ToolWrapperType = Callable[
|
|
17
|
+
[BaseTool, ToolCall, Any], dict[str, Any] | Command[Any] | None
|
|
18
|
+
]
|
|
19
|
+
AsyncToolWrapperType = Callable[
|
|
20
|
+
[BaseTool, ToolCall, Any],
|
|
21
|
+
Awaitable[dict[str, Any] | Command[Any] | None],
|
|
22
|
+
]
|
|
23
|
+
OutputType = dict[Literal["messages"], list[ToolMessage]] | Command[Any] | None
|
|
8
24
|
|
|
9
|
-
|
|
25
|
+
|
|
26
|
+
class UiPathToolNode(RunnableCallable):
|
|
27
|
+
"""
|
|
28
|
+
A ToolNode that can be used in a React agent graph.
|
|
29
|
+
It extracts the tool call from the state messages and invokes the tool.
|
|
30
|
+
It supports optional synchronous and asynchronous wrappers for custom processing.
|
|
31
|
+
Generic over the state model.
|
|
32
|
+
Args:
|
|
33
|
+
tool: The tool to invoke.
|
|
34
|
+
wrapper: An optional synchronous wrapper for custom processing.
|
|
35
|
+
awrapper: An optional asynchronous wrapper for custom processing.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A dict with ToolMessage or a Command.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
tool: BaseTool,
|
|
44
|
+
wrapper: ToolWrapperType | None = None,
|
|
45
|
+
awrapper: AsyncToolWrapperType | None = None,
|
|
46
|
+
):
|
|
47
|
+
super().__init__(func=self._func, afunc=self._afunc, name=tool.name)
|
|
48
|
+
self.tool = tool
|
|
49
|
+
self.wrapper = wrapper
|
|
50
|
+
self.awrapper = awrapper
|
|
51
|
+
|
|
52
|
+
def _func(self, state: Any, config: RunnableConfig | None = None) -> OutputType:
|
|
53
|
+
call = self._extract_tool_call(state)
|
|
54
|
+
if call is None:
|
|
55
|
+
return None
|
|
56
|
+
if self.wrapper:
|
|
57
|
+
filtered_state = self._filter_state(state, self.wrapper)
|
|
58
|
+
result = self.wrapper(self.tool, call, filtered_state)
|
|
59
|
+
else:
|
|
60
|
+
result = self.tool.invoke(call["args"])
|
|
61
|
+
return self._process_result(call, result)
|
|
62
|
+
|
|
63
|
+
async def _afunc(
|
|
64
|
+
self, state: Any, config: RunnableConfig | None = None
|
|
65
|
+
) -> OutputType:
|
|
66
|
+
call = self._extract_tool_call(state)
|
|
67
|
+
if call is None:
|
|
68
|
+
return None
|
|
69
|
+
if self.awrapper:
|
|
70
|
+
filtered_state = self._filter_state(state, self.awrapper)
|
|
71
|
+
result = await self.awrapper(self.tool, call, filtered_state)
|
|
72
|
+
else:
|
|
73
|
+
result = await self.tool.ainvoke(call["args"])
|
|
74
|
+
return self._process_result(call, result)
|
|
75
|
+
|
|
76
|
+
def _extract_tool_call(self, state: Any) -> ToolCall | None:
|
|
77
|
+
"""Extract the tool call from the state messages."""
|
|
78
|
+
|
|
79
|
+
if not hasattr(state, "messages"):
|
|
80
|
+
raise ValueError("State does not have messages key")
|
|
81
|
+
|
|
82
|
+
last_message = state.messages[-1]
|
|
83
|
+
if not isinstance(last_message, AIMessage):
|
|
84
|
+
raise ValueError("Last message in message stack is not an AIMessage.")
|
|
85
|
+
|
|
86
|
+
for tool_call in last_message.tool_calls:
|
|
87
|
+
if tool_call["name"] == self.tool.name:
|
|
88
|
+
return tool_call
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def _process_result(
|
|
92
|
+
self, call: ToolCall, result: dict[str, Any] | Command[Any] | None
|
|
93
|
+
) -> OutputType:
|
|
94
|
+
"""Process the tool result into a message format or return a Command."""
|
|
95
|
+
if isinstance(result, Command):
|
|
96
|
+
return result
|
|
97
|
+
else:
|
|
98
|
+
message = ToolMessage(
|
|
99
|
+
content=str(result), name=call["name"], tool_call_id=call["id"]
|
|
100
|
+
)
|
|
101
|
+
return {"messages": [message]}
|
|
102
|
+
|
|
103
|
+
def _filter_state(
|
|
104
|
+
self, state: Any, wrapper: ToolWrapperType | AsyncToolWrapperType
|
|
105
|
+
) -> BaseModel:
|
|
106
|
+
"""Filter the state to the expected model type."""
|
|
107
|
+
model_type = list(signature(wrapper).parameters.values())[2].annotation
|
|
108
|
+
if not issubclass(model_type, BaseModel):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"Wrapper state parameter must be a pydantic BaseModel subclass."
|
|
111
|
+
)
|
|
112
|
+
return model_type.model_validate(state, from_attributes=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ToolWrapperMixin:
|
|
116
|
+
wrapper: ToolWrapperType | None = None
|
|
117
|
+
awrapper: AsyncToolWrapperType | None = None
|
|
118
|
+
|
|
119
|
+
def set_tool_wrappers(
|
|
120
|
+
self,
|
|
121
|
+
wrapper: ToolWrapperType | None = None,
|
|
122
|
+
awrapper: AsyncToolWrapperType | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Define wrappers for the tool execution."""
|
|
125
|
+
self.wrapper = wrapper
|
|
126
|
+
self.awrapper = awrapper
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def create_tool_node(tools: Sequence[BaseTool]) -> dict[str, UiPathToolNode]:
|
|
10
130
|
"""Create individual ToolNode for each tool.
|
|
11
131
|
|
|
12
132
|
Args:
|
|
13
133
|
tools: Sequence of tools to create nodes for.
|
|
134
|
+
agentState: The type of the agent state model.
|
|
14
135
|
|
|
15
136
|
Returns:
|
|
16
|
-
Dict mapping tool.name ->
|
|
137
|
+
Dict mapping tool.name -> ReactToolNode([tool]).
|
|
17
138
|
Each tool gets its own dedicated node for middleware composition.
|
|
18
139
|
|
|
19
140
|
Note:
|
|
20
141
|
handle_tool_errors=False delegates error handling to LangGraph's error boundary.
|
|
21
142
|
"""
|
|
22
|
-
|
|
143
|
+
dict_mapping: dict[str, UiPathToolNode] = {}
|
|
144
|
+
for tool in tools:
|
|
145
|
+
if isinstance(tool, ToolWrapperMixin):
|
|
146
|
+
dict_mapping[tool.name] = UiPathToolNode(
|
|
147
|
+
tool, wrapper=tool.wrapper, awrapper=tool.awrapper
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
dict_mapping[tool.name] = UiPathToolNode(tool, wrapper=None, awrapper=None)
|
|
151
|
+
return dict_mapping
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tool-related utility functions."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def sanitize_tool_name(name: str) -> str:
|
|
@@ -9,3 +10,33 @@ def sanitize_tool_name(name: str) -> str:
|
|
|
9
10
|
sanitized_tool_name = re.sub(r"[^a-zA-Z0-9_-]", "", trim_whitespaces)
|
|
10
11
|
sanitized_tool_name = sanitized_tool_name[:64]
|
|
11
12
|
return sanitized_tool_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sanitize_dict_for_serialization(args: dict[str, Any]) -> dict[str, Any]:
|
|
16
|
+
"""Convert Pydantic models in args to dicts."""
|
|
17
|
+
converted_args: dict[str, Any] = {}
|
|
18
|
+
for key, value in args.items():
|
|
19
|
+
# handle Pydantic model
|
|
20
|
+
if hasattr(value, "model_dump"):
|
|
21
|
+
converted_args[key] = value.model_dump()
|
|
22
|
+
|
|
23
|
+
elif isinstance(value, list):
|
|
24
|
+
# handle list of Pydantic models
|
|
25
|
+
converted_list = []
|
|
26
|
+
for item in value:
|
|
27
|
+
if hasattr(item, "model_dump"):
|
|
28
|
+
converted_list.append(item.model_dump())
|
|
29
|
+
elif hasattr(item, "value"):
|
|
30
|
+
converted_list.append(item.value)
|
|
31
|
+
else:
|
|
32
|
+
converted_list.append(item)
|
|
33
|
+
converted_args[key] = converted_list
|
|
34
|
+
|
|
35
|
+
# handle enum-like objects with value attribute
|
|
36
|
+
elif hasattr(value, "value"):
|
|
37
|
+
converted_args[key] = value.value
|
|
38
|
+
|
|
39
|
+
# handle regular value or unexpected type
|
|
40
|
+
else:
|
|
41
|
+
converted_args[key] = value
|
|
42
|
+
return converted_args
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Wrappers to add behavior to tools while keeping them graph agnostic."""
|
|
2
|
+
|
|
3
|
+
from .job_attachment_wrapper import get_job_attachment_wrapper
|
|
4
|
+
from .static_args_wrapper import get_static_args_wrapper
|
|
5
|
+
|
|
6
|
+
__all__ = ["get_static_args_wrapper", "get_job_attachment_wrapper"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from langchain_core.messages.tool import ToolCall
|
|
4
|
+
from langchain_core.tools import BaseTool
|
|
5
|
+
from langgraph.types import Command
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from uipath_langchain.agent.react.job_attachments import (
|
|
9
|
+
get_job_attachment_paths,
|
|
10
|
+
replace_job_attachment_ids,
|
|
11
|
+
)
|
|
12
|
+
from uipath_langchain.agent.react.types import AgentGraphState
|
|
13
|
+
from uipath_langchain.agent.tools.tool_node import AsyncToolWrapperType
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_job_attachment_wrapper() -> AsyncToolWrapperType:
|
|
17
|
+
"""Create a tool wrapper that validates and replaces job attachment IDs with full attachment objects.
|
|
18
|
+
|
|
19
|
+
This wrapper extracts job attachment paths from the tool's schema, validates that all
|
|
20
|
+
referenced attachments exist in the agent state, and replaces attachment IDs with complete
|
|
21
|
+
attachment objects before invoking the tool.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
resource: The agent tool resource configuration
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
An async tool wrapper function that handles job attachment validation and replacement
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
async def job_attachment_wrapper(
|
|
31
|
+
tool: BaseTool,
|
|
32
|
+
call: ToolCall,
|
|
33
|
+
state: AgentGraphState,
|
|
34
|
+
) -> dict[str, Any] | Command[Any] | None:
|
|
35
|
+
"""Validate and replace job attachments in tool arguments before invocation.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
tool: The tool to wrap
|
|
39
|
+
call: The tool call containing arguments
|
|
40
|
+
state: The agent graph state containing job attachments
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Tool invocation result, or error dict if attachment validation fails
|
|
44
|
+
"""
|
|
45
|
+
input_args = call["args"]
|
|
46
|
+
modified_input_args = input_args
|
|
47
|
+
|
|
48
|
+
if isinstance(tool.args_schema, type) and issubclass(
|
|
49
|
+
tool.args_schema, BaseModel
|
|
50
|
+
):
|
|
51
|
+
errors: list[str] = []
|
|
52
|
+
paths = get_job_attachment_paths(tool.args_schema)
|
|
53
|
+
modified_input_args = replace_job_attachment_ids(
|
|
54
|
+
paths, input_args, state.job_attachments, errors
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if errors:
|
|
58
|
+
return {"error": "\n".join(errors)}
|
|
59
|
+
|
|
60
|
+
return await tool.ainvoke(modified_input_args)
|
|
61
|
+
|
|
62
|
+
return job_attachment_wrapper
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from langchain_core.messages.tool import ToolCall
|
|
4
|
+
from langchain_core.tools import BaseTool
|
|
5
|
+
from langgraph.types import Command
|
|
6
|
+
from uipath.agent.models.agent import BaseAgentResourceConfig
|
|
7
|
+
|
|
8
|
+
from uipath_langchain.agent.react.types import AgentGraphState
|
|
9
|
+
from uipath_langchain.agent.tools.static_args import handle_static_args
|
|
10
|
+
from uipath_langchain.agent.tools.tool_node import AsyncToolWrapperType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_static_args_wrapper(
|
|
14
|
+
resource: BaseAgentResourceConfig,
|
|
15
|
+
) -> AsyncToolWrapperType:
|
|
16
|
+
"""Returns an asynchronous tool wrapper that applies static arguments.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
resource: The agent resource configuration.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
An asynchronous tool wrapper function.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
async def static_args_wrapper(
|
|
26
|
+
tool: BaseTool,
|
|
27
|
+
call: ToolCall,
|
|
28
|
+
state: AgentGraphState,
|
|
29
|
+
) -> dict[str, Any] | Command[Any] | None:
|
|
30
|
+
input_args = call["args"]
|
|
31
|
+
merged_args = handle_static_args(resource, state, input_args)
|
|
32
|
+
return await tool.ainvoke(merged_args)
|
|
33
|
+
|
|
34
|
+
return static_args_wrapper
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from .mapper import UiPathChatMessagesMapper
|
|
2
2
|
from .models import UiPathAzureChatOpenAI, UiPathChat
|
|
3
3
|
from .openai import UiPathChatOpenAI
|
|
4
|
+
from .supported_models import BedrockModels, GeminiModels, OpenAIModels
|
|
4
5
|
|
|
5
6
|
__all__ = [
|
|
6
7
|
"UiPathChat",
|
|
7
8
|
"UiPathAzureChatOpenAI",
|
|
8
9
|
"UiPathChatOpenAI",
|
|
9
10
|
"UiPathChatMessagesMapper",
|
|
11
|
+
"OpenAIModels",
|
|
12
|
+
"BedrockModels",
|
|
13
|
+
"GeminiModels",
|
|
10
14
|
]
|
uipath_langchain/chat/bedrock.py
CHANGED
|
@@ -48,10 +48,14 @@ class AwsBedrockCompletionsPassthroughClient:
|
|
|
48
48
|
model: str,
|
|
49
49
|
token: str,
|
|
50
50
|
api_flavor: str,
|
|
51
|
+
agenthub_config: Optional[str] = None,
|
|
52
|
+
byo_connection_id: Optional[str] = None,
|
|
51
53
|
):
|
|
52
54
|
self.model = model
|
|
53
55
|
self.token = token
|
|
54
56
|
self.api_flavor = api_flavor
|
|
57
|
+
self.agenthub_config = agenthub_config
|
|
58
|
+
self.byo_connection_id = byo_connection_id
|
|
55
59
|
self._vendor = "awsbedrock"
|
|
56
60
|
self._url: Optional[str] = None
|
|
57
61
|
|
|
@@ -101,6 +105,10 @@ class AwsBedrockCompletionsPassthroughClient:
|
|
|
101
105
|
"X-UiPath-Streaming-Enabled": streaming,
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
if self.agenthub_config:
|
|
109
|
+
headers["X-UiPath-AgentHub-Config"] = self.agenthub_config
|
|
110
|
+
if self.byo_connection_id:
|
|
111
|
+
headers["X-UiPath-LlmGateway-ByoIsConnectionId"] = self.byo_connection_id
|
|
104
112
|
job_key = os.getenv("UIPATH_JOB_KEY")
|
|
105
113
|
process_key = os.getenv("UIPATH_PROCESS_KEY")
|
|
106
114
|
if job_key:
|
|
@@ -118,6 +126,8 @@ class UiPathChatBedrockConverse(ChatBedrockConverse):
|
|
|
118
126
|
tenant_id: Optional[str] = None,
|
|
119
127
|
token: Optional[str] = None,
|
|
120
128
|
model_name: str = BedrockModels.anthropic_claude_haiku_4_5,
|
|
129
|
+
agenthub_config: Optional[str] = None,
|
|
130
|
+
byo_connection_id: Optional[str] = None,
|
|
121
131
|
**kwargs,
|
|
122
132
|
):
|
|
123
133
|
org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
|
|
@@ -141,6 +151,8 @@ class UiPathChatBedrockConverse(ChatBedrockConverse):
|
|
|
141
151
|
model=model_name,
|
|
142
152
|
token=token,
|
|
143
153
|
api_flavor="converse",
|
|
154
|
+
agenthub_config=agenthub_config,
|
|
155
|
+
byo_connection_id=byo_connection_id,
|
|
144
156
|
)
|
|
145
157
|
|
|
146
158
|
client = passthrough_client.get_client()
|
|
@@ -156,6 +168,8 @@ class UiPathChatBedrock(ChatBedrock):
|
|
|
156
168
|
tenant_id: Optional[str] = None,
|
|
157
169
|
token: Optional[str] = None,
|
|
158
170
|
model_name: str = BedrockModels.anthropic_claude_haiku_4_5,
|
|
171
|
+
agenthub_config: Optional[str] = None,
|
|
172
|
+
byo_connection_id: Optional[str] = None,
|
|
159
173
|
**kwargs,
|
|
160
174
|
):
|
|
161
175
|
org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
|
|
@@ -179,6 +193,8 @@ class UiPathChatBedrock(ChatBedrock):
|
|
|
179
193
|
model=model_name,
|
|
180
194
|
token=token,
|
|
181
195
|
api_flavor="invoke",
|
|
196
|
+
agenthub_config=agenthub_config,
|
|
197
|
+
byo_connection_id=byo_connection_id,
|
|
182
198
|
)
|
|
183
199
|
|
|
184
200
|
client = passthrough_client.get_client()
|
uipath_langchain/chat/mapper.py
CHANGED
|
@@ -41,6 +41,7 @@ class UiPathChatMessagesMapper:
|
|
|
41
41
|
def __init__(self):
|
|
42
42
|
"""Initialize the mapper with empty state."""
|
|
43
43
|
self.tool_call_to_ai_message: dict[str, str] = {}
|
|
44
|
+
self.current_message: AIMessageChunk
|
|
44
45
|
self.seen_message_ids: set[str] = set()
|
|
45
46
|
|
|
46
47
|
def _extract_text(self, content: Any) -> str:
|
|
@@ -141,7 +142,7 @@ class UiPathChatMessagesMapper:
|
|
|
141
142
|
def map_event(
|
|
142
143
|
self,
|
|
143
144
|
message: BaseMessage,
|
|
144
|
-
) -> UiPathConversationMessageEvent | None:
|
|
145
|
+
) -> list[UiPathConversationMessageEvent] | None:
|
|
145
146
|
"""Convert LangGraph BaseMessage (chunk or full) into a UiPathConversationMessageEvent.
|
|
146
147
|
|
|
147
148
|
Args:
|
|
@@ -168,16 +169,45 @@ class UiPathChatMessagesMapper:
|
|
|
168
169
|
|
|
169
170
|
# Check if this is the last chunk by examining chunk_position
|
|
170
171
|
if message.chunk_position == "last":
|
|
172
|
+
events: list[UiPathConversationMessageEvent] = []
|
|
173
|
+
|
|
174
|
+
# Loop through all content_blocks in current_message and create toolCallStart events for each tool_call_chunk
|
|
175
|
+
if self.current_message and self.current_message.content_blocks:
|
|
176
|
+
for block in self.current_message.content_blocks:
|
|
177
|
+
if block.get("type") == "tool_call_chunk":
|
|
178
|
+
tool_chunk_block = cast(ToolCallChunk, block)
|
|
179
|
+
tool_call_id = tool_chunk_block.get("id")
|
|
180
|
+
tool_name = tool_chunk_block.get("name")
|
|
181
|
+
tool_args = tool_chunk_block.get("args")
|
|
182
|
+
|
|
183
|
+
if tool_call_id:
|
|
184
|
+
tool_event = UiPathConversationMessageEvent(
|
|
185
|
+
message_id=message.id,
|
|
186
|
+
tool_call=UiPathConversationToolCallEvent(
|
|
187
|
+
tool_call_id=tool_call_id,
|
|
188
|
+
start=UiPathConversationToolCallStartEvent(
|
|
189
|
+
tool_name=tool_name,
|
|
190
|
+
timestamp=timestamp,
|
|
191
|
+
input=UiPathInlineValue(inline=tool_args),
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
events.append(tool_event)
|
|
196
|
+
|
|
197
|
+
# Create the final event for the message
|
|
171
198
|
msg_event.end = UiPathConversationMessageEndEvent(timestamp=timestamp)
|
|
172
199
|
msg_event.content_part = UiPathConversationContentPartEvent(
|
|
173
200
|
content_part_id=f"chunk-{message.id}-0",
|
|
174
201
|
end=UiPathConversationContentPartEndEvent(),
|
|
175
202
|
)
|
|
176
|
-
|
|
203
|
+
events.append(msg_event)
|
|
204
|
+
|
|
205
|
+
return events
|
|
177
206
|
|
|
178
207
|
# For every new message_id, start a new message
|
|
179
208
|
if message.id not in self.seen_message_ids:
|
|
180
209
|
self.seen_message_ids.add(message.id)
|
|
210
|
+
self.current_message = message
|
|
181
211
|
msg_event.start = UiPathConversationMessageStartEvent(
|
|
182
212
|
role="assistant", timestamp=timestamp
|
|
183
213
|
)
|
|
@@ -200,7 +230,6 @@ class UiPathChatMessagesMapper:
|
|
|
200
230
|
content_part_id=f"chunk-{message.id}-0",
|
|
201
231
|
chunk=UiPathConversationContentPartChunkEvent(
|
|
202
232
|
data=text,
|
|
203
|
-
content_part_sequence=0,
|
|
204
233
|
),
|
|
205
234
|
)
|
|
206
235
|
|
|
@@ -210,19 +239,10 @@ class UiPathChatMessagesMapper:
|
|
|
210
239
|
tool_call_id = tool_chunk_block.get("id")
|
|
211
240
|
if tool_call_id:
|
|
212
241
|
# Track tool_call_id -> ai_message_id mapping
|
|
213
|
-
self.tool_call_to_ai_message[
|
|
214
|
-
|
|
215
|
-
args = tool_chunk_block.get("args") or ""
|
|
242
|
+
self.tool_call_to_ai_message[tool_call_id] = message.id
|
|
216
243
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
chunk=UiPathConversationContentPartChunkEvent(
|
|
220
|
-
data=args,
|
|
221
|
-
content_part_sequence=0,
|
|
222
|
-
),
|
|
223
|
-
)
|
|
224
|
-
# Continue so that multiple tool_call_chunks in the same block list
|
|
225
|
-
# are handled correctly
|
|
244
|
+
# Accumulate the message chunk
|
|
245
|
+
self.current_message = self.current_message + message
|
|
226
246
|
continue
|
|
227
247
|
|
|
228
248
|
# Fallback: raw string content on the chunk (rare when using content_blocks)
|
|
@@ -231,7 +251,6 @@ class UiPathChatMessagesMapper:
|
|
|
231
251
|
content_part_id=f"content-{message.id}",
|
|
232
252
|
chunk=UiPathConversationContentPartChunkEvent(
|
|
233
253
|
data=message.content,
|
|
234
|
-
content_part_sequence=0,
|
|
235
254
|
),
|
|
236
255
|
)
|
|
237
256
|
|
|
@@ -241,7 +260,7 @@ class UiPathChatMessagesMapper:
|
|
|
241
260
|
or msg_event.tool_call
|
|
242
261
|
or msg_event.end
|
|
243
262
|
):
|
|
244
|
-
return msg_event
|
|
263
|
+
return [msg_event]
|
|
245
264
|
|
|
246
265
|
return None
|
|
247
266
|
|
|
@@ -275,35 +294,34 @@ class UiPathChatMessagesMapper:
|
|
|
275
294
|
# Keep as string if not valid JSON
|
|
276
295
|
pass
|
|
277
296
|
|
|
278
|
-
return
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
297
|
+
return [
|
|
298
|
+
UiPathConversationMessageEvent(
|
|
299
|
+
message_id=result_message_id or str(uuid4()),
|
|
300
|
+
tool_call=UiPathConversationToolCallEvent(
|
|
301
|
+
tool_call_id=message.tool_call_id,
|
|
302
|
+
end=UiPathConversationToolCallEndEvent(
|
|
303
|
+
timestamp=timestamp,
|
|
304
|
+
output=UiPathInlineValue(inline=content_value),
|
|
305
|
+
),
|
|
286
306
|
),
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
output=UiPathInlineValue(inline=content_value),
|
|
290
|
-
),
|
|
291
|
-
),
|
|
292
|
-
)
|
|
307
|
+
)
|
|
308
|
+
]
|
|
293
309
|
|
|
294
310
|
# --- Fallback for other BaseMessage types ---
|
|
295
311
|
text_content = self._extract_text(message.content)
|
|
296
|
-
return
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
312
|
+
return [
|
|
313
|
+
UiPathConversationMessageEvent(
|
|
314
|
+
message_id=message.id,
|
|
315
|
+
start=UiPathConversationMessageStartEvent(
|
|
316
|
+
role="assistant", timestamp=timestamp
|
|
317
|
+
),
|
|
318
|
+
content_part=UiPathConversationContentPartEvent(
|
|
319
|
+
content_part_id=f"cp-{message.id}",
|
|
320
|
+
chunk=UiPathConversationContentPartChunkEvent(data=text_content),
|
|
321
|
+
),
|
|
322
|
+
end=UiPathConversationMessageEndEvent(),
|
|
323
|
+
)
|
|
324
|
+
]
|
|
307
325
|
|
|
308
326
|
|
|
309
327
|
__all__ = ["UiPathChatMessagesMapper"]
|