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.
Files changed (60) hide show
  1. uipath_langchain/_cli/_templates/langgraph.json.template +2 -4
  2. uipath_langchain/_cli/cli_new.py +1 -2
  3. uipath_langchain/_utils/_request_mixin.py +8 -0
  4. uipath_langchain/_utils/_settings.py +3 -2
  5. uipath_langchain/agent/guardrails/__init__.py +0 -16
  6. uipath_langchain/agent/guardrails/actions/__init__.py +2 -0
  7. uipath_langchain/agent/guardrails/actions/block_action.py +1 -1
  8. uipath_langchain/agent/guardrails/actions/escalate_action.py +265 -138
  9. uipath_langchain/agent/guardrails/actions/filter_action.py +290 -0
  10. uipath_langchain/agent/guardrails/actions/log_action.py +1 -1
  11. uipath_langchain/agent/guardrails/guardrail_nodes.py +193 -42
  12. uipath_langchain/agent/guardrails/guardrails_factory.py +235 -14
  13. uipath_langchain/agent/guardrails/types.py +0 -12
  14. uipath_langchain/agent/guardrails/utils.py +177 -0
  15. uipath_langchain/agent/react/agent.py +24 -9
  16. uipath_langchain/agent/react/constants.py +1 -2
  17. uipath_langchain/agent/react/file_type_handler.py +123 -0
  18. uipath_langchain/agent/{guardrails → react/guardrails}/guardrails_subgraph.py +119 -25
  19. uipath_langchain/agent/react/init_node.py +16 -1
  20. uipath_langchain/agent/react/job_attachments.py +125 -0
  21. uipath_langchain/agent/react/json_utils.py +183 -0
  22. uipath_langchain/agent/react/jsonschema_pydantic_converter.py +76 -0
  23. uipath_langchain/agent/react/llm_node.py +41 -10
  24. uipath_langchain/agent/react/llm_with_files.py +76 -0
  25. uipath_langchain/agent/react/router.py +48 -37
  26. uipath_langchain/agent/react/types.py +19 -1
  27. uipath_langchain/agent/react/utils.py +30 -4
  28. uipath_langchain/agent/tools/__init__.py +7 -1
  29. uipath_langchain/agent/tools/context_tool.py +151 -1
  30. uipath_langchain/agent/tools/escalation_tool.py +46 -15
  31. uipath_langchain/agent/tools/integration_tool.py +20 -16
  32. uipath_langchain/agent/tools/internal_tools/__init__.py +5 -0
  33. uipath_langchain/agent/tools/internal_tools/analyze_files_tool.py +113 -0
  34. uipath_langchain/agent/tools/internal_tools/internal_tool_factory.py +54 -0
  35. uipath_langchain/agent/tools/mcp_tool.py +86 -0
  36. uipath_langchain/agent/tools/process_tool.py +8 -1
  37. uipath_langchain/agent/tools/static_args.py +18 -40
  38. uipath_langchain/agent/tools/tool_factory.py +13 -5
  39. uipath_langchain/agent/tools/tool_node.py +133 -4
  40. uipath_langchain/agent/tools/utils.py +31 -0
  41. uipath_langchain/agent/wrappers/__init__.py +6 -0
  42. uipath_langchain/agent/wrappers/job_attachment_wrapper.py +62 -0
  43. uipath_langchain/agent/wrappers/static_args_wrapper.py +34 -0
  44. uipath_langchain/chat/__init__.py +4 -0
  45. uipath_langchain/chat/bedrock.py +16 -0
  46. uipath_langchain/chat/mapper.py +60 -42
  47. uipath_langchain/chat/openai.py +56 -26
  48. uipath_langchain/chat/supported_models.py +9 -0
  49. uipath_langchain/chat/vertex.py +62 -46
  50. uipath_langchain/embeddings/embeddings.py +18 -12
  51. uipath_langchain/runtime/factory.py +10 -5
  52. uipath_langchain/runtime/runtime.py +38 -35
  53. uipath_langchain/runtime/schema.py +72 -16
  54. uipath_langchain/runtime/storage.py +178 -71
  55. {uipath_langchain-0.1.28.dist-info → uipath_langchain-0.3.1.dist-info}/METADATA +7 -4
  56. uipath_langchain-0.3.1.dist-info/RECORD +90 -0
  57. uipath_langchain-0.1.28.dist-info/RECORD +0 -76
  58. {uipath_langchain-0.1.28.dist-info → uipath_langchain-0.3.1.dist-info}/WHEEL +0 -0
  59. {uipath_langchain-0.1.28.dist-info → uipath_langchain-0.3.1.dist-info}/entry_points.txt +0 -0
  60. {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.prebuilt import ToolNode
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
- def create_tool_node(tools: Sequence[BaseTool]) -> dict[str, ToolNode]:
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 -> ToolNode([tool]).
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
- return {tool.name: ToolNode([tool], handle_tool_errors=False) for tool in tools}
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
  ]
@@ -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()
@@ -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
- return msg_event
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[str(tool_call_id)] = message.id
214
-
215
- args = tool_chunk_block.get("args") or ""
242
+ self.tool_call_to_ai_message[tool_call_id] = message.id
216
243
 
217
- msg_event.content_part = UiPathConversationContentPartEvent(
218
- content_part_id=f"chunk-{message.id}-0",
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 UiPathConversationMessageEvent(
279
- message_id=result_message_id or str(uuid4()),
280
- tool_call=UiPathConversationToolCallEvent(
281
- tool_call_id=message.tool_call_id,
282
- start=UiPathConversationToolCallStartEvent(
283
- tool_name=message.name,
284
- arguments=None,
285
- timestamp=timestamp,
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
- end=UiPathConversationToolCallEndEvent(
288
- timestamp=timestamp,
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 UiPathConversationMessageEvent(
297
- message_id=message.id,
298
- start=UiPathConversationMessageStartEvent(
299
- role="assistant", timestamp=timestamp
300
- ),
301
- content_part=UiPathConversationContentPartEvent(
302
- content_part_id=f"cp-{message.id}",
303
- chunk=UiPathConversationContentPartChunkEvent(data=text_content),
304
- ),
305
- end=UiPathConversationMessageEndEvent(),
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"]