uipath-langchain 0.0.133__py3-none-any.whl → 0.1.28__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 (83) hide show
  1. uipath_langchain/_cli/cli_init.py +130 -191
  2. uipath_langchain/_cli/cli_new.py +2 -3
  3. uipath_langchain/_resources/AGENTS.md +21 -0
  4. uipath_langchain/_resources/REQUIRED_STRUCTURE.md +92 -0
  5. uipath_langchain/_tracing/__init__.py +3 -2
  6. uipath_langchain/_tracing/_instrument_traceable.py +11 -12
  7. uipath_langchain/_utils/_request_mixin.py +327 -51
  8. uipath_langchain/_utils/_settings.py +2 -2
  9. uipath_langchain/agent/exceptions/__init__.py +6 -0
  10. uipath_langchain/agent/exceptions/exceptions.py +11 -0
  11. uipath_langchain/agent/guardrails/__init__.py +21 -0
  12. uipath_langchain/agent/guardrails/actions/__init__.py +11 -0
  13. uipath_langchain/agent/guardrails/actions/base_action.py +24 -0
  14. uipath_langchain/agent/guardrails/actions/block_action.py +42 -0
  15. uipath_langchain/agent/guardrails/actions/escalate_action.py +499 -0
  16. uipath_langchain/agent/guardrails/actions/log_action.py +58 -0
  17. uipath_langchain/agent/guardrails/guardrail_nodes.py +173 -0
  18. uipath_langchain/agent/guardrails/guardrails_factory.py +70 -0
  19. uipath_langchain/agent/guardrails/guardrails_subgraph.py +283 -0
  20. uipath_langchain/agent/guardrails/types.py +20 -0
  21. uipath_langchain/agent/react/__init__.py +14 -0
  22. uipath_langchain/agent/react/agent.py +117 -0
  23. uipath_langchain/agent/react/constants.py +2 -0
  24. uipath_langchain/agent/react/init_node.py +20 -0
  25. uipath_langchain/agent/react/llm_node.py +43 -0
  26. uipath_langchain/agent/react/router.py +97 -0
  27. uipath_langchain/agent/react/terminate_node.py +82 -0
  28. uipath_langchain/agent/react/tools/__init__.py +7 -0
  29. uipath_langchain/agent/react/tools/tools.py +50 -0
  30. uipath_langchain/agent/react/types.py +39 -0
  31. uipath_langchain/agent/react/utils.py +49 -0
  32. uipath_langchain/agent/tools/__init__.py +17 -0
  33. uipath_langchain/agent/tools/context_tool.py +53 -0
  34. uipath_langchain/agent/tools/escalation_tool.py +111 -0
  35. uipath_langchain/agent/tools/integration_tool.py +181 -0
  36. uipath_langchain/agent/tools/process_tool.py +49 -0
  37. uipath_langchain/agent/tools/static_args.py +138 -0
  38. uipath_langchain/agent/tools/structured_tool_with_output_type.py +14 -0
  39. uipath_langchain/agent/tools/tool_factory.py +45 -0
  40. uipath_langchain/agent/tools/tool_node.py +22 -0
  41. uipath_langchain/agent/tools/utils.py +11 -0
  42. uipath_langchain/chat/__init__.py +4 -0
  43. uipath_langchain/chat/bedrock.py +187 -0
  44. uipath_langchain/chat/mapper.py +309 -0
  45. uipath_langchain/chat/models.py +248 -35
  46. uipath_langchain/chat/openai.py +133 -0
  47. uipath_langchain/chat/supported_models.py +42 -0
  48. uipath_langchain/chat/vertex.py +255 -0
  49. uipath_langchain/embeddings/embeddings.py +131 -34
  50. uipath_langchain/middlewares.py +0 -6
  51. uipath_langchain/retrievers/context_grounding_retriever.py +7 -9
  52. uipath_langchain/runtime/__init__.py +36 -0
  53. uipath_langchain/runtime/_serialize.py +46 -0
  54. uipath_langchain/runtime/config.py +61 -0
  55. uipath_langchain/runtime/errors.py +43 -0
  56. uipath_langchain/runtime/factory.py +315 -0
  57. uipath_langchain/runtime/graph.py +159 -0
  58. uipath_langchain/runtime/runtime.py +453 -0
  59. uipath_langchain/runtime/schema.py +386 -0
  60. uipath_langchain/runtime/storage.py +115 -0
  61. uipath_langchain/vectorstores/context_grounding_vectorstore.py +90 -110
  62. {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.28.dist-info}/METADATA +44 -23
  63. uipath_langchain-0.1.28.dist-info/RECORD +76 -0
  64. {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.28.dist-info}/WHEEL +1 -1
  65. uipath_langchain-0.1.28.dist-info/entry_points.txt +5 -0
  66. uipath_langchain/_cli/_runtime/_context.py +0 -21
  67. uipath_langchain/_cli/_runtime/_conversation.py +0 -298
  68. uipath_langchain/_cli/_runtime/_exception.py +0 -17
  69. uipath_langchain/_cli/_runtime/_input.py +0 -139
  70. uipath_langchain/_cli/_runtime/_output.py +0 -234
  71. uipath_langchain/_cli/_runtime/_runtime.py +0 -379
  72. uipath_langchain/_cli/_utils/_graph.py +0 -199
  73. uipath_langchain/_cli/cli_dev.py +0 -44
  74. uipath_langchain/_cli/cli_eval.py +0 -78
  75. uipath_langchain/_cli/cli_run.py +0 -82
  76. uipath_langchain/_tracing/_oteladapter.py +0 -222
  77. uipath_langchain/_tracing/_utils.py +0 -28
  78. uipath_langchain/builder/agent_config.py +0 -191
  79. uipath_langchain/tools/preconfigured.py +0 -191
  80. uipath_langchain-0.0.133.dist-info/RECORD +0 -41
  81. uipath_langchain-0.0.133.dist-info/entry_points.txt +0 -2
  82. /uipath_langchain/{tools/__init__.py → py.typed} +0 -0
  83. {uipath_langchain-0.0.133.dist-info → uipath_langchain-0.1.28.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,181 @@
1
+ """Process tool creation for UiPath process execution."""
2
+
3
+ import copy
4
+ from typing import Any
5
+
6
+ from jsonschema_pydantic_converter import transform as create_model
7
+ from langchain.tools import ToolRuntime
8
+ from langchain_core.tools import StructuredTool
9
+ from uipath.agent.models.agent import AgentIntegrationToolResourceConfig
10
+ from uipath.eval.mocks import mockable
11
+ from uipath.platform import UiPath
12
+ from uipath.platform.connections import ActivityMetadata, ActivityParameterLocationInfo
13
+
14
+ from .static_args import handle_static_args
15
+ from .structured_tool_with_output_type import StructuredToolWithOutputType
16
+ from .utils import sanitize_tool_name
17
+
18
+
19
+ def remove_asterisk_from_properties(fields: dict[str, Any]) -> dict[str, Any]:
20
+ """
21
+ Fix bug in integration service.
22
+ """
23
+ fields = copy.deepcopy(fields)
24
+
25
+ def fix_types(props: dict[str, Any]) -> None:
26
+ type_ = props.get("type", None)
27
+ if "$ref" in props:
28
+ props["$ref"] = props["$ref"].replace("[*]", "")
29
+ if type_ == "object":
30
+ properties = {}
31
+ for k, v in props.get("properties", {}).items():
32
+ # Remove asterisks!
33
+ k = k.replace("[*]", "")
34
+ properties[k] = v
35
+ if isinstance(v, dict):
36
+ fix_types(v)
37
+ if "properties" in props:
38
+ props["properties"] = properties
39
+ if type_ == "array":
40
+ fix_types(props.get("items", {}))
41
+
42
+ definitions = {}
43
+ for k, value in fields.get("$defs", fields.get("definitions", {})).items():
44
+ k = k.replace("[*]", "")
45
+ definitions[k] = value
46
+ fix_types(value)
47
+ if "definitions" in fields:
48
+ fields["definitions"] = definitions
49
+
50
+ fix_types(fields)
51
+ return fields
52
+
53
+
54
+ def extract_top_level_field(param_name: str) -> str:
55
+ """Extract the top-level field name from a jsonpath parameter name.
56
+
57
+ Examples:
58
+ metadata.field.test -> metadata
59
+ attachments[*] -> attachments
60
+ attachments[0].filename -> attachments
61
+ simple_field -> simple_field
62
+ """
63
+ # Split by '.' to get the first part
64
+ first_part = param_name.split(".")[0]
65
+
66
+ # Remove array notation if present (e.g., "attachments[*]" -> "attachments")
67
+ if "[" in first_part:
68
+ first_part = first_part.split("[")[0]
69
+
70
+ return first_part
71
+
72
+
73
+ def convert_to_activity_metadata(
74
+ resource: AgentIntegrationToolResourceConfig,
75
+ ) -> ActivityMetadata:
76
+ """Convert AgentIntegrationToolResourceConfig to ActivityMetadata."""
77
+
78
+ # normalize HTTP method (GETBYID -> GET)
79
+ http_method = resource.properties.method
80
+ if http_method == "GETBYID":
81
+ http_method = "GET"
82
+
83
+ param_location_info = ActivityParameterLocationInfo()
84
+ # because of nested fields and array notation, use a set to avoid duplicates
85
+ body_fields_set = set()
86
+
87
+ # mapping parameter locations
88
+ for param in resource.properties.parameters:
89
+ param_name = param.name
90
+ field_location = param.field_location
91
+
92
+ if field_location == "query":
93
+ param_location_info.query_params.append(param_name)
94
+ elif field_location == "path":
95
+ param_location_info.path_params.append(param_name)
96
+ elif field_location == "header":
97
+ param_location_info.header_params.append(param_name)
98
+ elif field_location in ("multipart", "file"):
99
+ param_location_info.multipart_params.append(param_name)
100
+ elif field_location == "body":
101
+ # extract top-level field from jsonpath parameter name
102
+ top_level_field = extract_top_level_field(param_name)
103
+ body_fields_set.add(top_level_field)
104
+ else:
105
+ # default to body field - extract top-level field
106
+ top_level_field = extract_top_level_field(param_name)
107
+ body_fields_set.add(top_level_field)
108
+
109
+ param_location_info.body_fields = list(body_fields_set)
110
+
111
+ # determine content type
112
+ content_type = "application/json"
113
+ if resource.properties.body_structure is not None:
114
+ shorthand_type = resource.properties.body_structure.get("contentType", "json")
115
+ if shorthand_type == "multipart":
116
+ content_type = "multipart/form-data"
117
+
118
+ return ActivityMetadata(
119
+ object_path=resource.properties.tool_path,
120
+ method_name=http_method,
121
+ content_type=content_type,
122
+ parameter_location_info=param_location_info,
123
+ )
124
+
125
+
126
+ def create_integration_tool(
127
+ resource: AgentIntegrationToolResourceConfig,
128
+ ) -> StructuredTool:
129
+ """Creates a StructuredTool for invoking an Integration Service connector activity."""
130
+ tool_name: str = sanitize_tool_name(resource.name)
131
+ if resource.properties.connection.id is None:
132
+ raise ValueError("Connection ID cannot be None for integration tool.")
133
+ connection_id: str = resource.properties.connection.id
134
+
135
+ activity_metadata = convert_to_activity_metadata(resource)
136
+
137
+ input_model = create_model(resource.input_schema)
138
+ # note: IS tools output schemas were recently added and are most likely not present in all resources
139
+ output_model: Any = (
140
+ create_model(remove_asterisk_from_properties(resource.output_schema))
141
+ if resource.output_schema
142
+ else create_model({"type": "object", "properties": {}})
143
+ )
144
+
145
+ sdk = UiPath()
146
+
147
+ @mockable(
148
+ name=resource.name,
149
+ description=resource.description,
150
+ input_schema=input_model.model_json_schema(),
151
+ output_schema=output_model.model_json_schema(),
152
+ )
153
+ async def integration_tool_fn(runtime: ToolRuntime, **kwargs: Any):
154
+ 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
+ result = await sdk.connections.invoke_activity_async(
164
+ activity_metadata=activity_metadata,
165
+ connection_id=connection_id,
166
+ activity_input=args,
167
+ )
168
+ except Exception:
169
+ raise
170
+
171
+ return result
172
+
173
+ tool = StructuredToolWithOutputType(
174
+ name=tool_name,
175
+ description=resource.description,
176
+ args_schema=resource.input_schema,
177
+ coroutine=integration_tool_fn,
178
+ output_type=output_model,
179
+ )
180
+
181
+ return tool
@@ -0,0 +1,49 @@
1
+ """Process tool creation for UiPath process execution."""
2
+
3
+ from typing import Any
4
+
5
+ from jsonschema_pydantic_converter import transform as create_model
6
+ from langchain_core.tools import StructuredTool
7
+ from langgraph.types import interrupt
8
+ from uipath.agent.models.agent import AgentProcessToolResourceConfig
9
+ from uipath.eval.mocks import mockable
10
+ from uipath.platform.common import InvokeProcess
11
+
12
+ from .structured_tool_with_output_type import StructuredToolWithOutputType
13
+ from .utils import sanitize_tool_name
14
+
15
+
16
+ def create_process_tool(resource: AgentProcessToolResourceConfig) -> StructuredTool:
17
+ """Uses interrupt() to suspend graph execution until process completes (handled by runtime)."""
18
+ tool_name: str = sanitize_tool_name(resource.name)
19
+ process_name = resource.properties.process_name
20
+ folder_path = resource.properties.folder_path
21
+
22
+ input_model: Any = create_model(resource.input_schema)
23
+ output_model: Any = create_model(resource.output_schema)
24
+
25
+ @mockable(
26
+ name=resource.name,
27
+ description=resource.description,
28
+ input_schema=input_model.model_json_schema(),
29
+ output_schema=output_model.model_json_schema(),
30
+ )
31
+ async def process_tool_fn(**kwargs: Any):
32
+ return interrupt(
33
+ InvokeProcess(
34
+ name=process_name,
35
+ input_arguments=kwargs,
36
+ process_folder_path=folder_path,
37
+ process_folder_key=None,
38
+ )
39
+ )
40
+
41
+ tool = StructuredToolWithOutputType(
42
+ name=tool_name,
43
+ description=resource.description,
44
+ args_schema=input_model,
45
+ coroutine=process_tool_fn,
46
+ output_type=output_model,
47
+ )
48
+
49
+ return tool
@@ -0,0 +1,138 @@
1
+ """Handles static arguments for tool calls."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from jsonpath_ng import parse # type: ignore[import-untyped]
6
+ from langchain.tools import ToolRuntime
7
+ from uipath.agent.models.agent import (
8
+ AgentIntegrationToolParameter,
9
+ AgentIntegrationToolResourceConfig,
10
+ BaseAgentResourceConfig,
11
+ )
12
+
13
+
14
+ def resolve_static_args(
15
+ resource: BaseAgentResourceConfig,
16
+ agent_input: Dict[str, Any],
17
+ ) -> Dict[str, Any]:
18
+ """Resolves static arguments for a given resource with a given input.
19
+
20
+ Args:
21
+ resource: The agent resource configuration.
22
+ input: Othe input arguments passed to the agent.
23
+
24
+ Returns:
25
+ A dictionary of expanded arguments to be used in the tool call.
26
+ static_args: Dict[str, Any] = {}
27
+ """
28
+
29
+ if isinstance(resource, AgentIntegrationToolResourceConfig):
30
+ return resolve_integration_static_args(
31
+ resource.properties.parameters, agent_input
32
+ )
33
+ else:
34
+ return {} # to be implemented for other resource types in the future
35
+
36
+
37
+ def resolve_integration_static_args(
38
+ parameters: List[AgentIntegrationToolParameter],
39
+ agent_input: Dict[str, Any],
40
+ ) -> Dict[str, Any]:
41
+ """Resolves static arguments for an integration tool resource.
42
+
43
+ Args:
44
+ resource: The AgentIntegrationToolResourceConfig instance.
45
+ input: The input arguments passed to the agent.
46
+
47
+ Returns:
48
+ A dictionary of expanded static arguments for the integration tool.
49
+ """
50
+
51
+ static_args: Dict[str, Any] = {}
52
+ for param in parameters:
53
+ value = None
54
+
55
+ # static parameter, use the defined static value
56
+ if param.field_variant == "static":
57
+ value = param.value
58
+ # argument parameter, extract value from agent input
59
+ elif param.field_variant == "argument":
60
+ if (
61
+ not isinstance(param.value, str)
62
+ or not param.value.startswith("{{")
63
+ or not param.value.endswith("}}")
64
+ ):
65
+ raise ValueError(
66
+ f"Parameter value must be in the format '{{argument_name}}' when field_variant is 'argument', got {param.value}"
67
+ )
68
+ arg_name = param.value[2:-2].strip()
69
+ # currently only support top-level arguments
70
+ value = agent_input.get(arg_name)
71
+
72
+ if value is not None:
73
+ static_args[param.name] = value
74
+
75
+ return static_args
76
+
77
+
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
+ def apply_static_args(
103
+ static_args: Dict[str, Any],
104
+ kwargs: Dict[str, Any],
105
+ ) -> Dict[str, Any]:
106
+ """Applies static arguments to the given input arguments.
107
+
108
+ Args:
109
+ static_args: Dictionary of static arguments {json_path: value} to apply.
110
+ kwargs: Original input arguments to the tool.
111
+
112
+ Returns:
113
+ Merged input arguments with static arguments applied.
114
+ """
115
+
116
+ sanitized_args = sanitize_for_serialization(kwargs)
117
+ for json_path, value in static_args.items():
118
+ expr = parse(json_path)
119
+ expr.update_or_create(sanitized_args, value)
120
+
121
+ return sanitized_args
122
+
123
+
124
+ def handle_static_args(
125
+ resource: BaseAgentResourceConfig, runtime: ToolRuntime, input_args: Dict[str, Any]
126
+ ) -> Dict[str, Any]:
127
+ """Resolves and applies static arguments for a tool call.
128
+ Args:
129
+ resource: The agent resource configuration.
130
+ runtime: The tool runtime providing the current state.
131
+ input_args: The original input arguments to the tool.
132
+ Returns:
133
+ A dictionary of input arguments with static arguments applied.
134
+ """
135
+
136
+ static_args = resolve_static_args(resource, dict(runtime.state))
137
+ merged_args = apply_static_args(static_args, input_args)
138
+ return merged_args
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+ from langchain_core.tools import StructuredTool
4
+ from pydantic import Field
5
+ from typing_extensions import override
6
+
7
+
8
+ class StructuredToolWithOutputType(StructuredTool):
9
+ output_type: Any = Field(Any, description="Output type.")
10
+
11
+ @override
12
+ @property
13
+ def OutputType(self) -> type[Any]:
14
+ return self.output_type
@@ -0,0 +1,45 @@
1
+ """Factory functions for creating tools from agent resources."""
2
+
3
+ from langchain_core.tools import BaseTool, StructuredTool
4
+ from uipath.agent.models.agent import (
5
+ AgentContextResourceConfig,
6
+ AgentEscalationResourceConfig,
7
+ AgentIntegrationToolResourceConfig,
8
+ AgentProcessToolResourceConfig,
9
+ BaseAgentResourceConfig,
10
+ LowCodeAgentDefinition,
11
+ )
12
+
13
+ from .context_tool import create_context_tool
14
+ from .escalation_tool import create_escalation_tool
15
+ from .integration_tool import create_integration_tool
16
+ from .process_tool import create_process_tool
17
+
18
+
19
+ async def create_tools_from_resources(agent: LowCodeAgentDefinition) -> list[BaseTool]:
20
+ tools: list[BaseTool] = []
21
+
22
+ for resource in agent.resources:
23
+ tool = await _build_tool_for_resource(resource)
24
+ if tool is not None:
25
+ tools.append(tool)
26
+
27
+ return tools
28
+
29
+
30
+ async def _build_tool_for_resource(
31
+ resource: BaseAgentResourceConfig,
32
+ ) -> StructuredTool | None:
33
+ if isinstance(resource, AgentProcessToolResourceConfig):
34
+ return create_process_tool(resource)
35
+
36
+ elif isinstance(resource, AgentContextResourceConfig):
37
+ return create_context_tool(resource)
38
+
39
+ elif isinstance(resource, AgentEscalationResourceConfig):
40
+ return create_escalation_tool(resource)
41
+
42
+ elif isinstance(resource, AgentIntegrationToolResourceConfig):
43
+ return create_integration_tool(resource)
44
+
45
+ return None
@@ -0,0 +1,22 @@
1
+ """Tool node factory wiring directly to LangGraph's ToolNode."""
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from langchain_core.tools import BaseTool
6
+ from langgraph.prebuilt import ToolNode
7
+
8
+
9
+ def create_tool_node(tools: Sequence[BaseTool]) -> dict[str, ToolNode]:
10
+ """Create individual ToolNode for each tool.
11
+
12
+ Args:
13
+ tools: Sequence of tools to create nodes for.
14
+
15
+ Returns:
16
+ Dict mapping tool.name -> ToolNode([tool]).
17
+ Each tool gets its own dedicated node for middleware composition.
18
+
19
+ Note:
20
+ handle_tool_errors=False delegates error handling to LangGraph's error boundary.
21
+ """
22
+ return {tool.name: ToolNode([tool], handle_tool_errors=False) for tool in tools}
@@ -0,0 +1,11 @@
1
+ """Tool-related utility functions."""
2
+
3
+ import re
4
+
5
+
6
+ def sanitize_tool_name(name: str) -> str:
7
+ """Sanitize tool name for LLM compatibility (alphanumeric, underscore, hyphen only, max 64 chars)."""
8
+ trim_whitespaces = "_".join(name.split())
9
+ sanitized_tool_name = re.sub(r"[^a-zA-Z0-9_-]", "", trim_whitespaces)
10
+ sanitized_tool_name = sanitized_tool_name[:64]
11
+ return sanitized_tool_name
@@ -1,6 +1,10 @@
1
+ from .mapper import UiPathChatMessagesMapper
1
2
  from .models import UiPathAzureChatOpenAI, UiPathChat
3
+ from .openai import UiPathChatOpenAI
2
4
 
3
5
  __all__ = [
4
6
  "UiPathChat",
5
7
  "UiPathAzureChatOpenAI",
8
+ "UiPathChatOpenAI",
9
+ "UiPathChatMessagesMapper",
6
10
  ]
@@ -0,0 +1,187 @@
1
+ import logging
2
+ import os
3
+ from typing import Optional
4
+
5
+ from uipath.utils import EndpointManager
6
+
7
+ from .supported_models import BedrockModels
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def _check_bedrock_dependencies() -> None:
13
+ """Check if required dependencies for UiPathChatBedrock are installed."""
14
+ import importlib.util
15
+
16
+ missing_packages = []
17
+
18
+ if importlib.util.find_spec("langchain_aws") is None:
19
+ missing_packages.append("langchain-aws")
20
+
21
+ if importlib.util.find_spec("boto3") is None:
22
+ missing_packages.append("boto3")
23
+
24
+ if missing_packages:
25
+ packages_str = ", ".join(missing_packages)
26
+ raise ImportError(
27
+ f"The following packages are required to use UiPathChatBedrock: {packages_str}\n"
28
+ "Please install them using one of the following methods:\n\n"
29
+ " # Using pip:\n"
30
+ f" pip install uipath-langchain[bedrock]\n\n"
31
+ " # Using uv:\n"
32
+ f" uv add 'uipath-langchain[bedrock]'\n\n"
33
+ )
34
+
35
+
36
+ _check_bedrock_dependencies()
37
+
38
+ import boto3
39
+ from langchain_aws import (
40
+ ChatBedrock,
41
+ ChatBedrockConverse,
42
+ )
43
+
44
+
45
+ class AwsBedrockCompletionsPassthroughClient:
46
+ def __init__(
47
+ self,
48
+ model: str,
49
+ token: str,
50
+ api_flavor: str,
51
+ ):
52
+ self.model = model
53
+ self.token = token
54
+ self.api_flavor = api_flavor
55
+ self._vendor = "awsbedrock"
56
+ self._url: Optional[str] = None
57
+
58
+ @property
59
+ def endpoint(self) -> str:
60
+ vendor_endpoint = EndpointManager.get_vendor_endpoint()
61
+ formatted_endpoint = vendor_endpoint.format(
62
+ vendor=self._vendor,
63
+ model=self.model,
64
+ )
65
+ return formatted_endpoint
66
+
67
+ def _build_base_url(self) -> str:
68
+ if not self._url:
69
+ env_uipath_url = os.getenv("UIPATH_URL")
70
+
71
+ if env_uipath_url:
72
+ self._url = f"{env_uipath_url.rstrip('/')}/{self.endpoint}"
73
+ else:
74
+ raise ValueError("UIPATH_URL environment variable is required")
75
+
76
+ return self._url
77
+
78
+ def get_client(self):
79
+ client = boto3.client(
80
+ "bedrock-runtime",
81
+ region_name="none",
82
+ aws_access_key_id="none",
83
+ aws_secret_access_key="none",
84
+ )
85
+ client.meta.events.register(
86
+ "before-send.bedrock-runtime.*", self._modify_request
87
+ )
88
+ return client
89
+
90
+ def _modify_request(self, request, **kwargs):
91
+ """Intercept boto3 request and redirect to LLM Gateway"""
92
+ # Detect streaming based on URL suffix:
93
+ # - converse-stream / invoke-with-response-stream -> streaming
94
+ # - converse / invoke -> non-streaming
95
+ streaming = "true" if request.url.endswith("-stream") else "false"
96
+ request.url = self._build_base_url()
97
+
98
+ headers = {
99
+ "Authorization": f"Bearer {self.token}",
100
+ "X-UiPath-LlmGateway-ApiFlavor": self.api_flavor,
101
+ "X-UiPath-Streaming-Enabled": streaming,
102
+ }
103
+
104
+ job_key = os.getenv("UIPATH_JOB_KEY")
105
+ process_key = os.getenv("UIPATH_PROCESS_KEY")
106
+ if job_key:
107
+ headers["X-UiPath-JobKey"] = job_key
108
+ if process_key:
109
+ headers["X-UiPath-ProcessKey"] = process_key
110
+
111
+ request.headers.update(headers)
112
+
113
+
114
+ class UiPathChatBedrockConverse(ChatBedrockConverse):
115
+ def __init__(
116
+ self,
117
+ org_id: Optional[str] = None,
118
+ tenant_id: Optional[str] = None,
119
+ token: Optional[str] = None,
120
+ model_name: str = BedrockModels.anthropic_claude_haiku_4_5,
121
+ **kwargs,
122
+ ):
123
+ org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
124
+ tenant_id = tenant_id or os.getenv("UIPATH_TENANT_ID")
125
+ token = token or os.getenv("UIPATH_ACCESS_TOKEN")
126
+
127
+ if not org_id:
128
+ raise ValueError(
129
+ "UIPATH_ORGANIZATION_ID environment variable or org_id parameter is required"
130
+ )
131
+ if not tenant_id:
132
+ raise ValueError(
133
+ "UIPATH_TENANT_ID environment variable or tenant_id parameter is required"
134
+ )
135
+ if not token:
136
+ raise ValueError(
137
+ "UIPATH_ACCESS_TOKEN environment variable or token parameter is required"
138
+ )
139
+
140
+ passthrough_client = AwsBedrockCompletionsPassthroughClient(
141
+ model=model_name,
142
+ token=token,
143
+ api_flavor="converse",
144
+ )
145
+
146
+ client = passthrough_client.get_client()
147
+ kwargs["client"] = client
148
+ kwargs["model"] = model_name
149
+ super().__init__(**kwargs)
150
+
151
+
152
+ class UiPathChatBedrock(ChatBedrock):
153
+ def __init__(
154
+ self,
155
+ org_id: Optional[str] = None,
156
+ tenant_id: Optional[str] = None,
157
+ token: Optional[str] = None,
158
+ model_name: str = BedrockModels.anthropic_claude_haiku_4_5,
159
+ **kwargs,
160
+ ):
161
+ org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
162
+ tenant_id = tenant_id or os.getenv("UIPATH_TENANT_ID")
163
+ token = token or os.getenv("UIPATH_ACCESS_TOKEN")
164
+
165
+ if not org_id:
166
+ raise ValueError(
167
+ "UIPATH_ORGANIZATION_ID environment variable or org_id parameter is required"
168
+ )
169
+ if not tenant_id:
170
+ raise ValueError(
171
+ "UIPATH_TENANT_ID environment variable or tenant_id parameter is required"
172
+ )
173
+ if not token:
174
+ raise ValueError(
175
+ "UIPATH_ACCESS_TOKEN environment variable or token parameter is required"
176
+ )
177
+
178
+ passthrough_client = AwsBedrockCompletionsPassthroughClient(
179
+ model=model_name,
180
+ token=token,
181
+ api_flavor="invoke",
182
+ )
183
+
184
+ client = passthrough_client.get_client()
185
+ kwargs["client"] = client
186
+ kwargs["model"] = model_name
187
+ super().__init__(**kwargs)