Skip to content

Commit 216fae1

Browse files
authored
Merge pull request #95 from UiPath/feat/add-chat-models
feat: add new chat models
2 parents 42603d3 + 894cdb7 commit 216fae1

14 files changed

Lines changed: 3328 additions & 1260 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,8 @@ cython_debug/
177177
**/.uipath
178178
**/**.nupkg
179179
**/__uipath/
180+
.claude/settings.local.json
181+
182+
/.vscode/launch.json
183+
184+
playground.py

pyproject.toml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-llamaindex"
3-
version = "0.1.5"
3+
version = "0.1.6"
44
description = "UiPath LlamaIndex SDK"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
@@ -24,6 +24,18 @@ maintainers = [
2424
{ name = "Cristian Pufu", email = "cristian.pufu@uipath.com" }
2525
]
2626

27+
[project.optional-dependencies]
28+
bedrock = [
29+
"llama-index-llms-bedrock>=0.3.0",
30+
"llama-index-llms-bedrock-converse>=0.3.0",
31+
"boto3>=1.28.0",
32+
"aiobotocore>=2.5.0",
33+
]
34+
vertex = [
35+
"llama-index-llms-google-genai>=0.8.0",
36+
"google-genai>=1.0.0",
37+
]
38+
2739
[project.entry-points."uipath.middlewares"]
2840
register = "uipath_llamaindex.middlewares:register_middleware"
2941

@@ -58,6 +70,8 @@ select = ["E", "F", "B", "I"]
5870

5971
[tool.ruff.lint.per-file-ignores]
6072
"*" = ["E501"]
73+
"src/uipath_llamaindex/llms/bedrock.py" = ["E402"]
74+
"src/uipath_llamaindex/llms/vertex.py" = ["E402"]
6175

6276
[tool.ruff.format]
6377
quote-style = "double"
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
from ._openai import (
1+
from ._openai import UiPathOpenAI
2+
from .supported_models import (
3+
BedrockModel,
4+
GeminiModel,
25
OpenAIModel,
3-
UiPathOpenAI,
46
)
57

68
__all__ = [
79
"UiPathOpenAI",
810
"OpenAIModel",
11+
"GeminiModel",
12+
"BedrockModel",
913
]

src/uipath_llamaindex/llms/_openai.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
11
import os
2-
from enum import Enum
32
from typing import Any
43

54
from llama_index.llms.azure_openai import AzureOpenAI # type: ignore
65
from uipath.utils import EndpointManager
76

8-
9-
class OpenAIModel(Enum):
10-
GPT_4_1_2025_04_14 = "gpt-4.1-2025-04-14"
11-
GPT_4_1_MINI_2025_04_14 = "gpt-4.1-mini-2025-04-14"
12-
GPT_4_1_NANO_2025_04_14 = "gpt-4.1-nano-2025-04-14"
13-
GPT_4O_2024_05_13 = "gpt-4o-2024-05-13"
14-
GPT_4O_2024_08_06 = "gpt-4o-2024-08-06"
15-
GPT_4O_2024_11_20 = "gpt-4o-2024-11-20"
16-
GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18"
17-
O3_MINI_2025_01_31 = "o3-mini-2025-01-31"
18-
TEXT_DAVINCI_003 = "text-davinci-003"
7+
from .supported_models import OpenAIModel
198

209

2110
class UiPathOpenAI(AzureOpenAI):
@@ -42,7 +31,7 @@ def __init__(
4231
defaults = {
4332
"model": model_value,
4433
"deployment_name": model_value,
45-
"azure_endpoint": f"{base_url}/{EndpointManager.get_passthrough_endpoint().format(model=model, api_version=api_version)}",
34+
"azure_endpoint": f"{base_url}/{EndpointManager.get_passthrough_endpoint().format(model=model_value, api_version=api_version)}",
4635
"api_key": os.environ.get("UIPATH_ACCESS_TOKEN"),
4736
"api_version": api_version,
4837
"is_chat_model": True,
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import logging
2+
import os
3+
from typing import Any, Optional, Sequence
4+
5+
from uipath.utils import EndpointManager
6+
7+
from .supported_models import BedrockModel
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
def _check_bedrock_dependencies() -> None:
13+
"""Check if required dependencies for UiPath Bedrock LLMs are installed."""
14+
import importlib.util
15+
16+
missing_packages = []
17+
18+
if importlib.util.find_spec("llama_index.llms.bedrock") is None:
19+
missing_packages.append("llama-index-llms-bedrock")
20+
21+
if importlib.util.find_spec("llama_index.llms.bedrock_converse") is None:
22+
missing_packages.append("llama-index-llms-bedrock-converse")
23+
24+
if importlib.util.find_spec("boto3") is None:
25+
missing_packages.append("boto3")
26+
27+
if importlib.util.find_spec("aiobotocore") is None:
28+
missing_packages.append("aiobotocore")
29+
30+
if missing_packages:
31+
packages_str = ", ".join(missing_packages)
32+
raise ImportError(
33+
f"The following packages are required to use UiPath Bedrock LLMs: {packages_str}\n"
34+
"Please install them using one of the following methods:\n\n"
35+
" # Using pip:\n"
36+
f" pip install uipath-llamaindex[bedrock]\n\n"
37+
" # Using uv:\n"
38+
f" uv add 'uipath-llamaindex[bedrock]'\n\n"
39+
)
40+
41+
42+
_check_bedrock_dependencies()
43+
44+
import boto3 # type: ignore[import-untyped]
45+
from llama_index.core.base.llms.types import ( # noqa: E402
46+
ChatMessage,
47+
ChatResponse,
48+
ChatResponseAsyncGen,
49+
CompletionResponse,
50+
CompletionResponseAsyncGen,
51+
)
52+
from llama_index.core.llms.callbacks import ( # noqa: E402
53+
llm_chat_callback,
54+
llm_completion_callback,
55+
)
56+
from llama_index.llms.bedrock import Bedrock # type: ignore[import-untyped]
57+
from llama_index.llms.bedrock_converse import ( # type: ignore[import-untyped]
58+
BedrockConverse,
59+
)
60+
61+
62+
class AwsBedrockCompletionsPassthroughClient:
63+
def __init__(
64+
self,
65+
model: str,
66+
token: str,
67+
api_flavor: str,
68+
):
69+
self.model = model
70+
self.token = token
71+
self.api_flavor = api_flavor
72+
self._vendor = "awsbedrock"
73+
self._url: Optional[str] = None
74+
75+
@property
76+
def endpoint(self) -> str:
77+
vendor_endpoint = EndpointManager.get_vendor_endpoint()
78+
formatted_endpoint = vendor_endpoint.format(
79+
vendor=self._vendor,
80+
model=self.model,
81+
)
82+
return formatted_endpoint
83+
84+
def _build_base_url(self) -> str:
85+
if not self._url:
86+
env_uipath_url = os.getenv("UIPATH_URL")
87+
88+
if env_uipath_url:
89+
self._url = f"{env_uipath_url.rstrip('/')}/{self.endpoint}"
90+
else:
91+
raise ValueError("UIPATH_URL environment variable is required")
92+
93+
return self._url
94+
95+
def get_client(self):
96+
client = boto3.client(
97+
"bedrock-runtime",
98+
region_name="us-east-1",
99+
aws_access_key_id="none",
100+
aws_secret_access_key="none",
101+
verify=True,
102+
)
103+
client.meta.events.register(
104+
"before-send.bedrock-runtime.*", self._modify_request
105+
)
106+
return client
107+
108+
def get_session(self):
109+
"""Get aiobotocore session for async operations with custom event handlers."""
110+
from aiobotocore.session import get_session # type: ignore[import-untyped]
111+
112+
session = get_session()
113+
session.get_component("event_emitter").register(
114+
"before-send.bedrock-runtime.*", self._modify_request
115+
)
116+
return session
117+
118+
def _modify_request(self, request, **kwargs):
119+
"""Intercept boto3 request and redirect to LLM Gateway"""
120+
# Detect streaming based on URL suffix:
121+
# - converse-stream / invoke-with-response-stream -> streaming
122+
# - converse / invoke -> non-streaming
123+
streaming = "true" if request.url.endswith("-stream") else "false"
124+
request.url = self._build_base_url()
125+
126+
headers = {
127+
"Authorization": f"Bearer {self.token}",
128+
"X-UiPath-LlmGateway-ApiFlavor": self.api_flavor,
129+
"X-UiPath-Streaming-Enabled": streaming,
130+
}
131+
132+
job_key = os.getenv("UIPATH_JOB_KEY")
133+
process_key = os.getenv("UIPATH_PROCESS_KEY")
134+
if job_key:
135+
headers["X-UiPath-JobKey"] = job_key
136+
if process_key:
137+
headers["X-UiPath-ProcessKey"] = process_key
138+
139+
request.headers.update(headers)
140+
141+
142+
class UiPathChatBedrockConverse(BedrockConverse):
143+
def __init__(
144+
self,
145+
org_id: Optional[str] = None,
146+
tenant_id: Optional[str] = None,
147+
token: Optional[str] = None,
148+
model: str = BedrockModel.anthropic_claude_haiku_4_5,
149+
**kwargs,
150+
):
151+
org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
152+
tenant_id = tenant_id or os.getenv("UIPATH_TENANT_ID")
153+
token = token or os.getenv("UIPATH_ACCESS_TOKEN")
154+
155+
if not org_id:
156+
raise ValueError(
157+
"UIPATH_ORGANIZATION_ID environment variable or org_id parameter is required"
158+
)
159+
if not tenant_id:
160+
raise ValueError(
161+
"UIPATH_TENANT_ID environment variable or tenant_id parameter is required"
162+
)
163+
if not token:
164+
raise ValueError(
165+
"UIPATH_ACCESS_TOKEN environment variable or token parameter is required"
166+
)
167+
168+
passthrough_client = AwsBedrockCompletionsPassthroughClient(
169+
model=model,
170+
token=token,
171+
api_flavor="converse",
172+
)
173+
174+
client = passthrough_client.get_client()
175+
botocore_session = passthrough_client.get_session()
176+
177+
super().__init__(
178+
model=model,
179+
client=client,
180+
botocore_session=botocore_session,
181+
region_name="us-east-1",
182+
aws_access_key_id="none",
183+
aws_secret_access_key="none",
184+
**kwargs,
185+
)
186+
187+
188+
class UiPathChatBedrock(Bedrock):
189+
def __init__(
190+
self,
191+
org_id: Optional[str] = None,
192+
tenant_id: Optional[str] = None,
193+
token: Optional[str] = None,
194+
model: str = BedrockModel.anthropic_claude_haiku_4_5,
195+
context_size: int = 200000,
196+
**kwargs,
197+
):
198+
org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
199+
tenant_id = tenant_id or os.getenv("UIPATH_TENANT_ID")
200+
token = token or os.getenv("UIPATH_ACCESS_TOKEN")
201+
202+
if not org_id:
203+
raise ValueError(
204+
"UIPATH_ORGANIZATION_ID environment variable or org_id parameter is required"
205+
)
206+
if not tenant_id:
207+
raise ValueError(
208+
"UIPATH_TENANT_ID environment variable or tenant_id parameter is required"
209+
)
210+
if not token:
211+
raise ValueError(
212+
"UIPATH_ACCESS_TOKEN environment variable or token parameter is required"
213+
)
214+
215+
passthrough_client = AwsBedrockCompletionsPassthroughClient(
216+
model=model,
217+
token=token,
218+
api_flavor="invoke",
219+
)
220+
221+
client = passthrough_client.get_client()
222+
223+
super().__init__(
224+
model=model,
225+
client=client,
226+
context_size=context_size,
227+
aws_access_key_id="none",
228+
aws_secret_access_key="none",
229+
region_name="us-east-1",
230+
**kwargs,
231+
)
232+
233+
@llm_completion_callback()
234+
async def acomplete(
235+
self, prompt: str, formatted: bool = False, **kwargs: Any
236+
) -> CompletionResponse:
237+
"""Async completion endpoint - delegates to sync complete."""
238+
return self.complete(prompt, formatted=formatted, **kwargs)
239+
240+
@llm_chat_callback()
241+
async def astream_chat(
242+
self, messages: Sequence[ChatMessage], **kwargs: Any
243+
) -> ChatResponseAsyncGen:
244+
"""Async streaming chat fallback - calls achat and yields single response."""
245+
246+
async def gen() -> ChatResponseAsyncGen:
247+
response = await self.achat(messages, **kwargs)
248+
yield ChatResponse(
249+
message=response.message,
250+
raw=response.raw,
251+
delta=response.message.content or "",
252+
additional_kwargs=response.additional_kwargs,
253+
)
254+
255+
return gen()
256+
257+
@llm_completion_callback()
258+
async def astream_complete(
259+
self, prompt: str, formatted: bool = False, **kwargs: Any
260+
) -> CompletionResponseAsyncGen:
261+
"""Async streaming completion fallback - calls acomplete and yields single response."""
262+
263+
async def gen() -> CompletionResponseAsyncGen:
264+
response = await self.acomplete(prompt, formatted=formatted, **kwargs)
265+
yield CompletionResponse(
266+
text=response.text,
267+
raw=response.raw,
268+
delta=response.text,
269+
additional_kwargs=response.additional_kwargs,
270+
)
271+
272+
return gen()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from enum import Enum
2+
3+
4+
class OpenAIModel(Enum):
5+
GPT_4_1_2025_04_14 = "gpt-4.1-2025-04-14"
6+
GPT_4_1_MINI_2025_04_14 = "gpt-4.1-mini-2025-04-14"
7+
GPT_4_1_NANO_2025_04_14 = "gpt-4.1-nano-2025-04-14"
8+
GPT_4O_2024_05_13 = "gpt-4o-2024-05-13"
9+
GPT_4O_2024_08_06 = "gpt-4o-2024-08-06"
10+
GPT_4O_2024_11_20 = "gpt-4o-2024-11-20"
11+
GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18"
12+
O3_MINI_2025_01_31 = "o3-mini-2025-01-31"
13+
TEXT_DAVINCI_003 = "text-davinci-003"
14+
15+
16+
class GeminiModel:
17+
"""Supported Google Gemini model identifiers."""
18+
19+
gemini_2_5_pro = "gemini-2.5-pro"
20+
gemini_2_5_flash = "gemini-2.5-flash"
21+
gemini_2_0_flash_001 = "gemini-2.0-flash-001"
22+
23+
24+
class BedrockModel:
25+
"""Supported AWS Bedrock model identifiers."""
26+
27+
# Claude 3.7 models
28+
anthropic_claude_3_7_sonnet = "anthropic.claude-3-7-sonnet-20250219-v1:0"
29+
30+
# Claude 4 models
31+
anthropic_claude_sonnet_4 = "anthropic.claude-sonnet-4-20250514-v1:0"
32+
anthropic_claude_sonnet_4_5 = "anthropic.claude-sonnet-4-5-20250929-v1:0"
33+
anthropic_claude_haiku_4_5 = "anthropic.claude-haiku-4-5-20251001-v1:0"

0 commit comments

Comments
 (0)