uipath-langchain 0.0.112__py3-none-any.whl → 0.1.24__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 (82) hide show
  1. uipath_langchain/_cli/_templates/main.py.template +12 -13
  2. uipath_langchain/_cli/cli_init.py +127 -156
  3. uipath_langchain/_cli/cli_new.py +2 -6
  4. uipath_langchain/_resources/AGENTS.md +21 -0
  5. uipath_langchain/_resources/REQUIRED_STRUCTURE.md +92 -0
  6. uipath_langchain/{tracers → _tracing}/__init__.py +0 -2
  7. uipath_langchain/_tracing/_instrument_traceable.py +134 -0
  8. uipath_langchain/_utils/__init__.py +1 -2
  9. uipath_langchain/_utils/_request_mixin.py +351 -54
  10. uipath_langchain/_utils/_settings.py +2 -11
  11. uipath_langchain/agent/exceptions/__init__.py +6 -0
  12. uipath_langchain/agent/exceptions/exceptions.py +11 -0
  13. uipath_langchain/agent/guardrails/__init__.py +21 -0
  14. uipath_langchain/agent/guardrails/actions/__init__.py +11 -0
  15. uipath_langchain/agent/guardrails/actions/base_action.py +23 -0
  16. uipath_langchain/agent/guardrails/actions/block_action.py +41 -0
  17. uipath_langchain/agent/guardrails/actions/escalate_action.py +274 -0
  18. uipath_langchain/agent/guardrails/actions/log_action.py +57 -0
  19. uipath_langchain/agent/guardrails/guardrail_nodes.py +125 -0
  20. uipath_langchain/agent/guardrails/guardrails_factory.py +70 -0
  21. uipath_langchain/agent/guardrails/guardrails_subgraph.py +247 -0
  22. uipath_langchain/agent/guardrails/types.py +20 -0
  23. uipath_langchain/agent/react/__init__.py +14 -0
  24. uipath_langchain/agent/react/agent.py +113 -0
  25. uipath_langchain/agent/react/constants.py +2 -0
  26. uipath_langchain/agent/react/init_node.py +20 -0
  27. uipath_langchain/agent/react/llm_node.py +43 -0
  28. uipath_langchain/agent/react/router.py +97 -0
  29. uipath_langchain/agent/react/terminate_node.py +82 -0
  30. uipath_langchain/agent/react/tools/__init__.py +7 -0
  31. uipath_langchain/agent/react/tools/tools.py +50 -0
  32. uipath_langchain/agent/react/types.py +39 -0
  33. uipath_langchain/agent/react/utils.py +49 -0
  34. uipath_langchain/agent/tools/__init__.py +17 -0
  35. uipath_langchain/agent/tools/context_tool.py +53 -0
  36. uipath_langchain/agent/tools/escalation_tool.py +111 -0
  37. uipath_langchain/agent/tools/integration_tool.py +181 -0
  38. uipath_langchain/agent/tools/process_tool.py +49 -0
  39. uipath_langchain/agent/tools/static_args.py +138 -0
  40. uipath_langchain/agent/tools/structured_tool_with_output_type.py +14 -0
  41. uipath_langchain/agent/tools/tool_factory.py +45 -0
  42. uipath_langchain/agent/tools/tool_node.py +22 -0
  43. uipath_langchain/agent/tools/utils.py +11 -0
  44. uipath_langchain/chat/__init__.py +4 -0
  45. uipath_langchain/chat/bedrock.py +187 -0
  46. uipath_langchain/chat/gemini.py +330 -0
  47. uipath_langchain/chat/mapper.py +309 -0
  48. uipath_langchain/chat/models.py +261 -38
  49. uipath_langchain/chat/openai.py +132 -0
  50. uipath_langchain/chat/supported_models.py +42 -0
  51. uipath_langchain/embeddings/embeddings.py +136 -36
  52. uipath_langchain/middlewares.py +0 -2
  53. uipath_langchain/py.typed +0 -0
  54. uipath_langchain/retrievers/context_grounding_retriever.py +7 -9
  55. uipath_langchain/runtime/__init__.py +36 -0
  56. uipath_langchain/runtime/_serialize.py +46 -0
  57. uipath_langchain/runtime/config.py +61 -0
  58. uipath_langchain/runtime/errors.py +43 -0
  59. uipath_langchain/runtime/factory.py +315 -0
  60. uipath_langchain/runtime/graph.py +159 -0
  61. uipath_langchain/runtime/runtime.py +453 -0
  62. uipath_langchain/runtime/schema.py +349 -0
  63. uipath_langchain/runtime/storage.py +115 -0
  64. uipath_langchain/vectorstores/context_grounding_vectorstore.py +90 -110
  65. {uipath_langchain-0.0.112.dist-info → uipath_langchain-0.1.24.dist-info}/METADATA +42 -20
  66. uipath_langchain-0.1.24.dist-info/RECORD +76 -0
  67. {uipath_langchain-0.0.112.dist-info → uipath_langchain-0.1.24.dist-info}/WHEEL +1 -1
  68. uipath_langchain-0.1.24.dist-info/entry_points.txt +5 -0
  69. uipath_langchain/_cli/_runtime/_context.py +0 -21
  70. uipath_langchain/_cli/_runtime/_exception.py +0 -17
  71. uipath_langchain/_cli/_runtime/_input.py +0 -136
  72. uipath_langchain/_cli/_runtime/_output.py +0 -234
  73. uipath_langchain/_cli/_runtime/_runtime.py +0 -371
  74. uipath_langchain/_cli/_utils/_graph.py +0 -202
  75. uipath_langchain/_cli/cli_run.py +0 -80
  76. uipath_langchain/tracers/AsyncUiPathTracer.py +0 -274
  77. uipath_langchain/tracers/_events.py +0 -33
  78. uipath_langchain/tracers/_instrument_traceable.py +0 -416
  79. uipath_langchain/tracers/_utils.py +0 -52
  80. uipath_langchain-0.0.112.dist-info/RECORD +0 -36
  81. uipath_langchain-0.0.112.dist-info/entry_points.txt +0 -2
  82. {uipath_langchain-0.0.112.dist-info → uipath_langchain-0.1.24.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,309 @@
1
+ import json
2
+ import logging
3
+ from datetime import datetime, timezone
4
+ from typing import Any, cast
5
+ from uuid import uuid4
6
+
7
+ from langchain_core.messages import (
8
+ AIMessageChunk,
9
+ BaseMessage,
10
+ HumanMessage,
11
+ TextContentBlock,
12
+ ToolCallChunk,
13
+ ToolMessage,
14
+ )
15
+ from pydantic import ValidationError
16
+ from uipath.core.chat import (
17
+ UiPathConversationContentPartChunkEvent,
18
+ UiPathConversationContentPartEndEvent,
19
+ UiPathConversationContentPartEvent,
20
+ UiPathConversationContentPartStartEvent,
21
+ UiPathConversationMessage,
22
+ UiPathConversationMessageEndEvent,
23
+ UiPathConversationMessageEvent,
24
+ UiPathConversationMessageStartEvent,
25
+ UiPathConversationToolCallEndEvent,
26
+ UiPathConversationToolCallEvent,
27
+ UiPathConversationToolCallStartEvent,
28
+ UiPathInlineValue,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class UiPathChatMessagesMapper:
35
+ """Stateful mapper that converts LangChain messages to UiPath message events.
36
+
37
+ Maintains state across multiple message conversions to properly track:
38
+ - The AI message ID associated with each tool call for proper correlation with ToolMessage
39
+ """
40
+
41
+ def __init__(self):
42
+ """Initialize the mapper with empty state."""
43
+ self.tool_call_to_ai_message: dict[str, str] = {}
44
+ self.seen_message_ids: set[str] = set()
45
+
46
+ def _extract_text(self, content: Any) -> str:
47
+ """Normalize LangGraph message.content to plain text."""
48
+ if isinstance(content, str):
49
+ return content
50
+ if isinstance(content, list):
51
+ return "".join(
52
+ part.get("text", "")
53
+ for part in content
54
+ if isinstance(part, dict) and part.get("type") == "text"
55
+ )
56
+ return str(content or "")
57
+
58
+ def map_messages(self, messages: list[Any]) -> list[Any]:
59
+ """Normalize any 'messages' list into LangChain messages.
60
+
61
+ - If already BaseMessage instances: return as-is.
62
+ - If UiPathConversationMessage: convert to HumanMessage.
63
+ """
64
+ if not isinstance(messages, list):
65
+ raise TypeError("messages must be a list")
66
+
67
+ if not messages:
68
+ return []
69
+
70
+ first = messages[0]
71
+
72
+ # Case 1: already LangChain messages
73
+ if isinstance(first, BaseMessage):
74
+ return cast(list[BaseMessage], messages)
75
+
76
+ # Case 2: UiPath messages -> convert to HumanMessage
77
+ if isinstance(first, UiPathConversationMessage):
78
+ if not all(isinstance(m, UiPathConversationMessage) for m in messages):
79
+ raise TypeError("Mixed message types not supported")
80
+ return self._map_messages_internal(
81
+ cast(list[UiPathConversationMessage], messages)
82
+ )
83
+
84
+ # Case3: List[dict] -> parse to List[UiPathConversationMessage]
85
+ if isinstance(first, dict):
86
+ try:
87
+ parsed_messages = [
88
+ UiPathConversationMessage.model_validate(message)
89
+ for message in messages
90
+ ]
91
+ return self._map_messages_internal(parsed_messages)
92
+ except ValidationError:
93
+ pass
94
+
95
+ # Fallback: unknown type – just pass through
96
+ return messages
97
+
98
+ def _map_messages_internal(
99
+ self, messages: list[UiPathConversationMessage]
100
+ ) -> list[HumanMessage]:
101
+ """
102
+ Converts a UiPathConversationMessage into a list of HumanMessages for LangGraph.
103
+ Supports multimodal content parts (text, external content) and preserves metadata.
104
+ """
105
+ human_messages: list[HumanMessage] = []
106
+
107
+ for uipath_msg in messages:
108
+ # Loop over each content part
109
+ if uipath_msg.content_parts:
110
+ for part in uipath_msg.content_parts:
111
+ data = part.data
112
+ content = ""
113
+ metadata: dict[str, Any] = {
114
+ "message_id": uipath_msg.message_id,
115
+ "content_part_id": part.content_part_id,
116
+ "mime_type": part.mime_type,
117
+ "created_at": uipath_msg.created_at,
118
+ "updated_at": uipath_msg.updated_at,
119
+ }
120
+
121
+ if isinstance(data, UiPathInlineValue):
122
+ content = str(data.inline)
123
+
124
+ # Append a HumanMessage for this content part
125
+ human_messages.append(
126
+ HumanMessage(content=content, metadata=metadata)
127
+ )
128
+
129
+ # Handle the case where there are no content parts
130
+ else:
131
+ metadata = {
132
+ "message_id": uipath_msg.message_id,
133
+ "role": uipath_msg.role,
134
+ "created_at": uipath_msg.created_at,
135
+ "updated_at": uipath_msg.updated_at,
136
+ }
137
+ human_messages.append(HumanMessage(content="", metadata=metadata))
138
+
139
+ return human_messages
140
+
141
+ def map_event(
142
+ self,
143
+ message: BaseMessage,
144
+ ) -> UiPathConversationMessageEvent | None:
145
+ """Convert LangGraph BaseMessage (chunk or full) into a UiPathConversationMessageEvent.
146
+
147
+ Args:
148
+ message: The LangChain message to convert
149
+
150
+ Returns:
151
+ A UiPathConversationMessageEvent if the message should be emitted, None otherwise.
152
+ """
153
+ # Format timestamp as ISO 8601 UTC with milliseconds: 2025-01-04T10:30:00.123Z
154
+ timestamp = (
155
+ datetime.now(timezone.utc)
156
+ .isoformat(timespec="milliseconds")
157
+ .replace("+00:00", "Z")
158
+ )
159
+
160
+ # --- Streaming AIMessageChunk ---
161
+ if isinstance(message, AIMessageChunk):
162
+ if message.id is None:
163
+ return None
164
+
165
+ msg_event = UiPathConversationMessageEvent(
166
+ message_id=message.id,
167
+ )
168
+
169
+ # Check if this is the last chunk by examining chunk_position
170
+ if message.chunk_position == "last":
171
+ msg_event.end = UiPathConversationMessageEndEvent(timestamp=timestamp)
172
+ msg_event.content_part = UiPathConversationContentPartEvent(
173
+ content_part_id=f"chunk-{message.id}-0",
174
+ end=UiPathConversationContentPartEndEvent(),
175
+ )
176
+ return msg_event
177
+
178
+ # For every new message_id, start a new message
179
+ if message.id not in self.seen_message_ids:
180
+ self.seen_message_ids.add(message.id)
181
+ msg_event.start = UiPathConversationMessageStartEvent(
182
+ role="assistant", timestamp=timestamp
183
+ )
184
+ msg_event.content_part = UiPathConversationContentPartEvent(
185
+ content_part_id=f"chunk-{message.id}-0",
186
+ start=UiPathConversationContentPartStartEvent(
187
+ mime_type="text/plain"
188
+ ),
189
+ )
190
+
191
+ elif message.content_blocks:
192
+ for block in message.content_blocks:
193
+ block_type = block.get("type")
194
+
195
+ if block_type == "text":
196
+ text_block = cast(TextContentBlock, block)
197
+ text = text_block["text"]
198
+
199
+ msg_event.content_part = UiPathConversationContentPartEvent(
200
+ content_part_id=f"chunk-{message.id}-0",
201
+ chunk=UiPathConversationContentPartChunkEvent(
202
+ data=text,
203
+ content_part_sequence=0,
204
+ ),
205
+ )
206
+
207
+ elif block_type == "tool_call_chunk":
208
+ tool_chunk_block = cast(ToolCallChunk, block)
209
+
210
+ tool_call_id = tool_chunk_block.get("id")
211
+ if tool_call_id:
212
+ # 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 ""
216
+
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
226
+ continue
227
+
228
+ # Fallback: raw string content on the chunk (rare when using content_blocks)
229
+ elif isinstance(message.content, str) and message.content:
230
+ msg_event.content_part = UiPathConversationContentPartEvent(
231
+ content_part_id=f"content-{message.id}",
232
+ chunk=UiPathConversationContentPartChunkEvent(
233
+ data=message.content,
234
+ content_part_sequence=0,
235
+ ),
236
+ )
237
+
238
+ if (
239
+ msg_event.start
240
+ or msg_event.content_part
241
+ or msg_event.tool_call
242
+ or msg_event.end
243
+ ):
244
+ return msg_event
245
+
246
+ return None
247
+
248
+ # --- ToolMessage ---
249
+ if isinstance(message, ToolMessage):
250
+ # Look up the AI message ID using the tool_call_id
251
+ result_message_id = (
252
+ self.tool_call_to_ai_message.get(message.tool_call_id)
253
+ if message.tool_call_id
254
+ else None
255
+ )
256
+
257
+ # If no AI message ID was found, we cannot properly associate this tool result
258
+ if not result_message_id:
259
+ logger.warning(
260
+ f"Tool message {message.tool_call_id} has no associated AI message ID. Skipping."
261
+ )
262
+
263
+ # Clean up the mapping after use
264
+ if (
265
+ message.tool_call_id
266
+ and message.tool_call_id in self.tool_call_to_ai_message
267
+ ):
268
+ del self.tool_call_to_ai_message[message.tool_call_id]
269
+
270
+ content_value: Any = message.content
271
+ if isinstance(content_value, str):
272
+ try:
273
+ content_value = json.loads(content_value)
274
+ except (json.JSONDecodeError, TypeError):
275
+ # Keep as string if not valid JSON
276
+ pass
277
+
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,
286
+ ),
287
+ end=UiPathConversationToolCallEndEvent(
288
+ timestamp=timestamp,
289
+ output=UiPathInlineValue(inline=content_value),
290
+ ),
291
+ ),
292
+ )
293
+
294
+ # --- Fallback for other BaseMessage types ---
295
+ 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
+ )
307
+
308
+
309
+ __all__ = ["UiPathChatMessagesMapper"]