Skip to content

Commit 44806f1

Browse files
committed
feat: add chat models
1 parent 3626d1d commit 44806f1

19 files changed

Lines changed: 4925 additions & 142 deletions

.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

playground.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from llama_index.core.llms import ChatMessage
2+
3+
from uipath_llamaindex.llms import GeminiModels, UiPathVertex
4+
5+
6+
def test_all_methods():
7+
llm = UiPathVertex(model=GeminiModels.gemini_2_5_flash, max_tokens=1024)
8+
prompt = "What is 2+2? Answer in one word."
9+
messages = [ChatMessage(role="user", content=prompt)]
10+
11+
results = {}
12+
13+
# Test complete
14+
print("Testing complete...")
15+
try:
16+
response = llm.complete(prompt)
17+
print(f" complete: {response.text.strip()}")
18+
results["complete"] = "PASS"
19+
except Exception as e:
20+
print(f" complete: FAILED - {e}")
21+
results["complete"] = "FAIL"
22+
23+
# Test chat
24+
print("Testing chat...")
25+
try:
26+
response = llm.chat(messages)
27+
print(f" chat: {response.message.content.strip()}")
28+
results["chat"] = "PASS"
29+
except Exception as e:
30+
print(f" chat: FAILED - {e}")
31+
results["chat"] = "FAIL"
32+
33+
# Test stream_complete
34+
print("Testing stream_complete...")
35+
try:
36+
chunks = []
37+
for chunk in llm.stream_complete(prompt):
38+
chunks.append(chunk.delta)
39+
print(f" stream_complete: {''.join(chunks).strip()}")
40+
results["stream_complete"] = "PASS"
41+
except Exception as e:
42+
print(f" stream_complete: FAILED - {e}")
43+
results["stream_complete"] = "FAIL"
44+
45+
# Test stream_chat
46+
print("Testing stream_chat...")
47+
try:
48+
chunks = []
49+
for chunk in llm.stream_chat(messages):
50+
chunks.append(chunk.delta)
51+
print(f" stream_chat: {''.join(chunks).strip()}")
52+
results["stream_chat"] = "PASS"
53+
except Exception as e:
54+
print(f" stream_chat: FAILED - {e}")
55+
results["stream_chat"] = "FAIL"
56+
57+
# Print summary
58+
print("\n" + "=" * 50)
59+
print("SUMMARY")
60+
print("=" * 50)
61+
62+
passed = sum(1 for v in results.values() if v == "PASS")
63+
failed = sum(1 for v in results.values() if v == "FAIL")
64+
65+
for method, status in results.items():
66+
icon = "+" if status == "PASS" else "x"
67+
print(f" [{icon}] {method}: {status}")
68+
69+
print("-" * 50)
70+
print(f" Total: {len(results)} | Passed: {passed} | Failed: {failed}")
71+
print("=" * 50)
72+
73+
74+
if __name__ == "__main__":
75+
test_all_methods()

pyproject.toml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
[project]
22
name = "uipath-llamaindex"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
description = "UiPath LlamaIndex SDK"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"
77
dependencies = [
8+
"aiosqlite>=0.21.0",
89
"llama-index>=0.14.8",
910
"llama-index-embeddings-azure-openai>=0.4.1",
1011
"llama-index-llms-azure-openai>=0.4.2",
12+
"llama-index-llms-google-genai>=0.8.0",
1113
"openinference-instrumentation-llama-index>=4.3.9",
12-
"uipath>=2.2.16, <2.3.0",
14+
"uipath>=2.2.26, <2.3.0",
1315
]
1416
classifiers = [
1517
"Intended Audience :: Developers",
@@ -23,6 +25,18 @@ maintainers = [
2325
{ name = "Cristian Pufu", email = "cristian.pufu@uipath.com" }
2426
]
2527

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

src/uipath_llamaindex/llms/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@
22
OpenAIModel,
33
UiPathOpenAI,
44
)
5+
from .supported_models import (
6+
BedrockModels,
7+
GeminiModels,
8+
OpenAIModels,
9+
)
10+
11+
# Note: UiPathVertex requires optional dependencies (google-genai, llama-index-llms-google-genai)
12+
# Import it directly from uipath_llamaindex.llms.vertex:
13+
# from uipath_llamaindex.llms.vertex import UiPathVertex
14+
15+
# Note: UiPathChatBedrock and UiPathChatBedrockConverse require optional dependencies
16+
# (boto3, aiobotocore, llama-index-llms-bedrock, llama-index-llms-bedrock-converse)
17+
# Import them directly from uipath_llamaindex.llms.bedrock:
18+
# from uipath_llamaindex.llms.bedrock import UiPathChatBedrock, UiPathChatBedrockConverse
519

620
__all__ = [
721
"UiPathOpenAI",
822
"OpenAIModel",
23+
"OpenAIModels",
24+
"GeminiModels",
25+
"BedrockModels",
926
]
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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 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
45+
from llama_index.llms.bedrock import Bedrock
46+
from llama_index.llms.bedrock_converse import BedrockConverse
47+
48+
49+
class AwsBedrockCompletionsPassthroughClient:
50+
def __init__(
51+
self,
52+
model: str,
53+
token: str,
54+
api_flavor: str,
55+
):
56+
self.model = model
57+
self.token = token
58+
self.api_flavor = api_flavor
59+
self._vendor = "awsbedrock"
60+
self._url: Optional[str] = None
61+
62+
@property
63+
def endpoint(self) -> str:
64+
vendor_endpoint = EndpointManager.get_vendor_endpoint()
65+
formatted_endpoint = vendor_endpoint.format(
66+
vendor=self._vendor,
67+
model=self.model,
68+
)
69+
return formatted_endpoint
70+
71+
def _build_base_url(self) -> str:
72+
if not self._url:
73+
env_uipath_url = os.getenv("UIPATH_URL")
74+
75+
if env_uipath_url:
76+
self._url = f"{env_uipath_url.rstrip('/')}/{self.endpoint}"
77+
else:
78+
raise ValueError("UIPATH_URL environment variable is required")
79+
80+
return self._url
81+
82+
def get_client(self):
83+
client = boto3.client(
84+
"bedrock-runtime",
85+
region_name="us-east-1",
86+
aws_access_key_id="none",
87+
aws_secret_access_key="none",
88+
verify=False,
89+
)
90+
client.meta.events.register(
91+
"before-send.bedrock-runtime.*", self._modify_request
92+
)
93+
return client
94+
95+
def get_session(self):
96+
"""Get aiobotocore session for async operations with custom event handlers."""
97+
from aiobotocore.session import get_session
98+
99+
session = get_session()
100+
session.get_component("event_emitter").register(
101+
"before-send.bedrock-runtime.*", self._modify_request
102+
)
103+
return session
104+
105+
def _modify_request(self, request, **kwargs):
106+
"""Intercept boto3 request and redirect to LLM Gateway"""
107+
# Detect streaming based on URL suffix:
108+
# - converse-stream / invoke-with-response-stream -> streaming
109+
# - converse / invoke -> non-streaming
110+
streaming = "true" if request.url.endswith("-stream") else "false"
111+
request.url = self._build_base_url()
112+
113+
headers = {
114+
"Authorization": f"Bearer {self.token}",
115+
"X-UiPath-LlmGateway-ApiFlavor": self.api_flavor,
116+
"X-UiPath-Streaming-Enabled": streaming,
117+
}
118+
119+
job_key = os.getenv("UIPATH_JOB_KEY")
120+
process_key = os.getenv("UIPATH_PROCESS_KEY")
121+
if job_key:
122+
headers["X-UiPath-JobKey"] = job_key
123+
if process_key:
124+
headers["X-UiPath-ProcessKey"] = process_key
125+
126+
request.headers.update(headers)
127+
128+
129+
class UiPathChatBedrockConverse(BedrockConverse):
130+
def __init__(
131+
self,
132+
org_id: Optional[str] = None,
133+
tenant_id: Optional[str] = None,
134+
token: Optional[str] = None,
135+
model: str = BedrockModels.anthropic_claude_haiku_4_5,
136+
**kwargs,
137+
):
138+
org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
139+
tenant_id = tenant_id or os.getenv("UIPATH_TENANT_ID")
140+
token = token or os.getenv("UIPATH_ACCESS_TOKEN")
141+
142+
if not org_id:
143+
raise ValueError(
144+
"UIPATH_ORGANIZATION_ID environment variable or org_id parameter is required"
145+
)
146+
if not tenant_id:
147+
raise ValueError(
148+
"UIPATH_TENANT_ID environment variable or tenant_id parameter is required"
149+
)
150+
if not token:
151+
raise ValueError(
152+
"UIPATH_ACCESS_TOKEN environment variable or token parameter is required"
153+
)
154+
155+
passthrough_client = AwsBedrockCompletionsPassthroughClient(
156+
model=model,
157+
token=token,
158+
api_flavor="converse",
159+
)
160+
161+
client = passthrough_client.get_client()
162+
botocore_session = passthrough_client.get_session()
163+
164+
super().__init__(
165+
model=model,
166+
client=client,
167+
botocore_session=botocore_session,
168+
region_name="us-east-1",
169+
aws_access_key_id="none",
170+
aws_secret_access_key="none",
171+
**kwargs,
172+
)
173+
174+
175+
class UiPathChatBedrock(Bedrock):
176+
def __init__(
177+
self,
178+
org_id: Optional[str] = None,
179+
tenant_id: Optional[str] = None,
180+
token: Optional[str] = None,
181+
model: str = BedrockModels.anthropic_claude_haiku_4_5,
182+
context_size: int = 200000,
183+
**kwargs,
184+
):
185+
org_id = org_id or os.getenv("UIPATH_ORGANIZATION_ID")
186+
tenant_id = tenant_id or os.getenv("UIPATH_TENANT_ID")
187+
token = token or os.getenv("UIPATH_ACCESS_TOKEN")
188+
189+
if not org_id:
190+
raise ValueError(
191+
"UIPATH_ORGANIZATION_ID environment variable or org_id parameter is required"
192+
)
193+
if not tenant_id:
194+
raise ValueError(
195+
"UIPATH_TENANT_ID environment variable or tenant_id parameter is required"
196+
)
197+
if not token:
198+
raise ValueError(
199+
"UIPATH_ACCESS_TOKEN environment variable or token parameter is required"
200+
)
201+
202+
passthrough_client = AwsBedrockCompletionsPassthroughClient(
203+
model=model,
204+
token=token,
205+
api_flavor="invoke",
206+
)
207+
208+
client = passthrough_client.get_client()
209+
210+
super().__init__(
211+
model=model,
212+
client=client,
213+
context_size=context_size,
214+
aws_access_key_id="none",
215+
aws_secret_access_key="none",
216+
region_name="us-east-1",
217+
**kwargs,
218+
)

0 commit comments

Comments
 (0)