Skip to content

Commit caf1b40

Browse files
authored
Merge pull request #173 from UiPath/feat/google-adk-uipath-llm-providers
feat: add UiPath LLM gateway providers for Google ADK
2 parents 4769b04 + 142af60 commit caf1b40

13 files changed

Lines changed: 1142 additions & 7 deletions

File tree

packages/uipath-google-adk/pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-google-adk"
3-
version = "0.0.2"
3+
version = "0.0.3"
44
description = "Python SDK that enables developers to build and deploy Google ADK agents to the UiPath Cloud Platform"
55
readme = "README.md"
66
requires-python = ">=3.11"
@@ -21,6 +21,9 @@ maintainers = [
2121
{ name = "Cristian Pufu", email = "cristian.pufu@uipath.com" }
2222
]
2323

24+
[project.optional-dependencies]
25+
anthropic = ["anthropic>=0.43.0"]
26+
2427
[project.entry-points."uipath.middlewares"]
2528
register = "uipath_google_adk.middlewares:register_middleware"
2629

@@ -93,6 +96,10 @@ module = "openinference.*"
9396
ignore_missing_imports = true
9497
ignore_errors = true
9598

99+
[[tool.mypy.overrides]]
100+
module = "anthropic.*"
101+
ignore_missing_imports = true
102+
96103
[tool.pytest.ini_options]
97104
testpaths = ["tests"]
98105
python_files = "test_*.py"

packages/uipath-google-adk/samples/quickstart-agent/.env.example

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/uipath-google-adk/samples/quickstart-agent/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import httpx
22
from google.adk.agents import Agent
33

4+
from uipath_google_adk.chat import UiPathGemini
5+
6+
# from uipath_google_adk.chat import UiPathOpenAI
7+
# from uipath_google_adk.chat import UiPathAnthropic
8+
49

510
def get_weather(location: str) -> str:
611
"""Get the current weather for a location using the Open-Meteo API.
@@ -55,7 +60,9 @@ def get_weather(location: str) -> str:
5560

5661
agent = Agent(
5762
name="weather_agent",
58-
model="gemini-2.5-flash",
63+
model=UiPathGemini(model="gemini-2.5-flash"),
64+
# model=UiPathOpenAI(model="gpt-4o-mini-2024-07-18"),
65+
# model=UiPathAnthropic(model="anthropic.claude-haiku-4-5-20251001-v1:0"),
5966
instruction="You are a helpful weather assistant. Use the get_weather tool to provide weather information.",
6067
tools=[get_weather],
6168
)

packages/uipath-google-adk/samples/quickstart-agent/pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Quickstart Google ADK agent example"
55
readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
8-
"uipath-google-adk>=0.0.1, <0.1.0",
8+
"uipath-google-adk[anthropic]>=0.0.1, <0.1.0",
99
"google-adk>=1.25.0",
1010
"uipath>=2.8.18, <2.9.0",
1111
]
@@ -17,3 +17,7 @@ dev = [
1717

1818
[tool.uv]
1919
override-dependencies = ["opentelemetry-sdk>=1.39.0,<1.40.0"]
20+
21+
[tool.uv.sources]
22+
uipath-dev = { path = "../../../../../uipath-dev-python", editable = true }
23+
uipath-google-adk = { path = "../../", editable = true }
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://cloud.uipath.com/draft/2024-12/uipath",
33
"runtimeOptions": {
4-
"isConversational": false
4+
"isConversational": true
55
},
66
"packOptions": {
77
"fileExtensionsIncluded": [],
@@ -11,4 +11,4 @@
1111
"includeUvLock": true
1212
},
1313
"functions": {}
14-
}
14+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
GOOGLE_API_KEY=your_gemini_api_key_here
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
11
"""UiPath Google ADK Runtime Integration."""
2+
3+
4+
def __getattr__(name):
5+
if name in ("UiPathOpenAI", "UiPathGemini", "UiPathAnthropic"):
6+
from . import chat
7+
8+
return getattr(chat, name)
9+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
10+
11+
12+
__all__ = [
13+
"UiPathOpenAI",
14+
"UiPathGemini",
15+
"UiPathAnthropic",
16+
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
UiPath Google ADK Chat module.
3+
4+
Provides LLM implementations that route through UiPath's LLM Gateway (AgentHub)
5+
for use with Google ADK agents.
6+
7+
NOTE: This module uses lazy imports via __getattr__ to avoid loading heavy
8+
dependencies (httpx, anthropic SDK) at import time.
9+
"""
10+
11+
12+
def __getattr__(name):
13+
if name == "UiPathOpenAI":
14+
from .openai import UiPathOpenAI
15+
16+
return UiPathOpenAI
17+
if name == "UiPathGemini":
18+
from .gemini import UiPathGemini
19+
20+
return UiPathGemini
21+
if name == "UiPathAnthropic":
22+
from .anthropic import UiPathAnthropic
23+
24+
return UiPathAnthropic
25+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
26+
27+
28+
__all__ = [
29+
"UiPathOpenAI",
30+
"UiPathGemini",
31+
"UiPathAnthropic",
32+
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Shared utilities for UiPath Google ADK chat providers."""
2+
3+
import os
4+
from typing import Optional
5+
6+
from uipath.utils import EndpointManager
7+
8+
9+
def get_uipath_config() -> tuple[str, str]:
10+
"""Read UiPath configuration from environment variables.
11+
12+
Returns:
13+
Tuple of (uipath_url, access_token).
14+
15+
Raises:
16+
ValueError: If required environment variables are not set.
17+
"""
18+
uipath_url = os.getenv("UIPATH_URL")
19+
if not uipath_url:
20+
raise ValueError("UIPATH_URL environment variable is required")
21+
22+
access_token = os.getenv("UIPATH_ACCESS_TOKEN")
23+
if not access_token:
24+
raise ValueError("UIPATH_ACCESS_TOKEN environment variable is required")
25+
26+
return uipath_url, access_token
27+
28+
29+
def get_uipath_headers(token: str) -> dict[str, str]:
30+
"""Build HTTP headers for UiPath Gateway requests.
31+
32+
Args:
33+
token: UiPath access token.
34+
35+
Returns:
36+
Dictionary of HTTP headers.
37+
"""
38+
headers = {
39+
"Authorization": f"Bearer {token}",
40+
}
41+
if job_key := os.getenv("UIPATH_JOB_KEY"):
42+
headers["X-UiPath-JobKey"] = job_key
43+
if process_key := os.getenv("UIPATH_PROCESS_KEY"):
44+
headers["X-UiPath-ProcessKey"] = process_key
45+
return headers
46+
47+
48+
def build_gateway_url(
49+
vendor: str,
50+
model: str,
51+
uipath_url: Optional[str] = None,
52+
) -> str:
53+
"""Build the full URL for the UiPath LLM Gateway.
54+
55+
Args:
56+
vendor: The LLM vendor (e.g., "openai", "vertexai", "anthropic").
57+
model: The model name.
58+
uipath_url: Optional UiPath URL. If not provided, reads from UIPATH_URL env var.
59+
60+
Returns:
61+
The full gateway URL.
62+
63+
Raises:
64+
ValueError: If UIPATH_URL is not set.
65+
"""
66+
if not uipath_url:
67+
uipath_url = os.getenv("UIPATH_URL")
68+
if not uipath_url:
69+
raise ValueError("UIPATH_URL environment variable is required")
70+
71+
vendor_endpoint = EndpointManager.get_vendor_endpoint()
72+
formatted_endpoint = vendor_endpoint.format(
73+
vendor=vendor,
74+
model=model,
75+
)
76+
return f"{uipath_url.rstrip('/')}/{formatted_endpoint}"
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""UiPath Anthropic LLM for Google ADK agents.
2+
3+
Wraps the Google ADK ``AnthropicLlm`` class, overriding the
4+
``_anthropic_client`` cached property to create an ``AsyncAnthropic``
5+
client that routes all requests through UiPath's LLM Gateway using
6+
the Bedrock invoke format.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import json
12+
import logging
13+
from functools import cached_property
14+
15+
import httpx
16+
from google.adk.models.anthropic_llm import AnthropicLlm
17+
from typing_extensions import override
18+
from uipath._utils._ssl_context import get_httpx_client_kwargs
19+
20+
from ._common import build_gateway_url, get_uipath_config, get_uipath_headers
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
def _check_anthropic_dependency() -> None:
26+
"""Check that the anthropic SDK is installed."""
27+
import importlib.util
28+
29+
if importlib.util.find_spec("anthropic") is None:
30+
raise ImportError(
31+
"The 'anthropic' package is required to use UiPathAnthropic.\n"
32+
"Install it with:\n\n"
33+
" pip install 'uipath-google-adk[anthropic]'\n\n"
34+
" # or with uv:\n"
35+
" uv add 'uipath-google-adk[anthropic]'\n"
36+
)
37+
38+
39+
class _AsyncUrlRewriteTransport(httpx.AsyncHTTPTransport):
40+
"""Async transport that rewrites Anthropic SDK requests to UiPath gateway.
41+
42+
Intercepts the Anthropic SDK's ``/v1/messages`` endpoint and:
43+
1. Rewrites the URL to the UiPath gateway
44+
2. Transforms the body to Bedrock invoke format (removes ``model``,
45+
adds ``anthropic_version``)
46+
47+
Extends ``httpx.AsyncHTTPTransport`` directly (proven pattern from
48+
``uipath-openai-agents``) and mutates ``request.url`` in-place.
49+
"""
50+
51+
def __init__(self, gateway_url: str, **kwargs):
52+
self.gateway_url = gateway_url
53+
super().__init__(**kwargs)
54+
55+
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
56+
url_str = str(request.url)
57+
# Intercept Anthropic SDK's /v1/messages endpoint
58+
if "/v1/messages" in url_str:
59+
gateway_parsed = httpx.URL(self.gateway_url)
60+
61+
# Transform body to Bedrock invoke format
62+
body = json.loads(request.content)
63+
body.pop("model", None)
64+
body.setdefault("anthropic_version", "bedrock-2023-05-31")
65+
is_streaming = body.get("stream", False)
66+
new_content = json.dumps(body).encode()
67+
68+
headers = dict(request.headers)
69+
headers["host"] = gateway_parsed.host
70+
headers["content-length"] = str(len(new_content))
71+
if is_streaming:
72+
headers["X-UiPath-Streaming-Enabled"] = "true"
73+
74+
# Create new request with modified URL and body
75+
request = httpx.Request(
76+
method=request.method,
77+
url=gateway_parsed,
78+
headers=headers,
79+
content=new_content,
80+
extensions=request.extensions,
81+
)
82+
83+
return await super().handle_async_request(request)
84+
85+
86+
class UiPathAnthropic(AnthropicLlm):
87+
"""Anthropic LLM that routes requests through UiPath's LLM Gateway.
88+
89+
Subclasses Google ADK's ``AnthropicLlm`` and overrides
90+
``_anthropic_client`` to use the Bedrock invoke format through
91+
the UiPath gateway (matching the ``uipath-llamaindex`` implementation).
92+
93+
Uses Bedrock model names (``anthropic.claude-*``) with the ``awsbedrock``
94+
vendor and ``invoke`` API flavor.
95+
96+
Example::
97+
98+
from uipath_google_adk.chat import UiPathAnthropic
99+
from google.adk.agents import Agent
100+
101+
agent = Agent(
102+
name="assistant",
103+
model=UiPathAnthropic(model="anthropic.claude-haiku-4-5-20251001-v1:0"),
104+
instruction="You are a helpful assistant.",
105+
)
106+
"""
107+
108+
@cached_property
109+
@override
110+
def _anthropic_client(self):
111+
_check_anthropic_dependency()
112+
113+
from anthropic import AsyncAnthropic # type: ignore[import-not-found]
114+
115+
uipath_url, token = get_uipath_config()
116+
effective_model = self.model
117+
gateway_url = build_gateway_url("awsbedrock", effective_model, uipath_url)
118+
auth_headers = get_uipath_headers(token)
119+
auth_headers["X-UiPath-LlmGateway-ApiFlavor"] = "invoke"
120+
client_kwargs = get_httpx_client_kwargs()
121+
verify = client_kwargs.get("verify", True)
122+
123+
http_client = httpx.AsyncClient(
124+
transport=_AsyncUrlRewriteTransport(gateway_url, verify=verify),
125+
**client_kwargs,
126+
)
127+
128+
return AsyncAnthropic(
129+
api_key="uipath-gateway",
130+
default_headers=auth_headers,
131+
http_client=http_client,
132+
)

0 commit comments

Comments
 (0)