Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-langchain"
version = "0.9.35"
version = "0.9.36"
description = "Python SDK that enables developers to build and deploy LangGraph agents to the UiPath Cloud Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
43 changes: 32 additions & 11 deletions src/uipath_langchain/chat/chat_model_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
}


def _should_skip_temperature(model_info: dict[str, Any]) -> bool:
details = model_info.get("modelDetails") or {}
return bool(details.get("shouldSkipTemperature", False))


def _fetch_discovery(agenthub_config: str) -> list[dict[str, Any]]:
"""Fetch available models from LLM Gateway discovery endpoint."""
from uipath.platform import UiPath
Expand All @@ -34,7 +39,7 @@ def _fetch_discovery(agenthub_config: str) -> list[dict[str, Any]]:
def _create_openai_llm(
model: str,
api_flavor: APIFlavor,
temperature: float,
temperature: float | None,
max_tokens: int,
agenthub_config: str,
byo_connection_id: str | None = None,
Expand All @@ -45,29 +50,33 @@ def _create_openai_llm(

azure_open_ai_latest_api_version = "2025-04-01-preview"

sampling_kwargs: dict[str, Any] = {}
if temperature is not None:
sampling_kwargs["temperature"] = temperature

match api_flavor:
case APIFlavor.OPENAI_RESPONSES:
return UiPathChatOpenAI(
use_responses_api=True,
model_name=model,
temperature=temperature,
max_tokens=max_tokens,
api_version=azure_open_ai_latest_api_version,
agenthub_config=agenthub_config,
byo_connection_id=byo_connection_id,
output_version="v1",
**sampling_kwargs,
**kwargs,
)
case APIFlavor.OPENAI_COMPLETIONS:
return UiPathChatOpenAI(
use_responses_api=False,
model_name=model,
temperature=temperature,
max_tokens=max_tokens,
api_version=azure_open_ai_latest_api_version,
agenthub_config=agenthub_config,
byo_connection_id=byo_connection_id,
output_version="v1",
**sampling_kwargs,
**kwargs,
)
case _:
Expand All @@ -77,7 +86,7 @@ def _create_openai_llm(
def _create_bedrock_llm(
model: str,
api_flavor: APIFlavor,
temperature: float,
temperature: float | None,
max_tokens: int,
agenthub_config: str,
byo_connection_id: str | None = None,
Expand All @@ -89,25 +98,29 @@ def _create_bedrock_llm(
UiPathChatBedrockConverse,
)

sampling_kwargs: dict[str, Any] = {}
if temperature is not None:
sampling_kwargs["temperature"] = temperature

match api_flavor:
case APIFlavor.AWS_BEDROCK_CONVERSE:
return UiPathChatBedrockConverse(
model_name=model,
temperature=temperature,
max_tokens=max_tokens,
agenthub_config=agenthub_config,
byo_connection_id=byo_connection_id,
output_version="v1",
**sampling_kwargs,
**kwargs,
)
case APIFlavor.AWS_BEDROCK_INVOKE:
return UiPathChatBedrock(
model_name=model,
temperature=temperature,
max_tokens=max_tokens,
agenthub_config=agenthub_config,
byo_connection_id=byo_connection_id,
output_version="v1",
**sampling_kwargs,
**kwargs,
)
case _:
Expand All @@ -117,7 +130,7 @@ def _create_bedrock_llm(
def _create_vertex_llm(
model: str,
api_flavor: APIFlavor,
temperature: float,
temperature: float | None,
max_tokens: int | None,
agenthub_config: str,
byo_connection_id: str | None = None,
Expand All @@ -126,15 +139,19 @@ def _create_vertex_llm(
"""Create UiPathChatVertex for Gemini models via LLMGateway."""
from uipath_langchain.chat.vertex import UiPathChatVertex

sampling_kwargs: dict[str, Any] = {}
if temperature is not None:
sampling_kwargs["temperature"] = temperature

match api_flavor:
case APIFlavor.VERTEX_GEMINI_GENERATE_CONTENT:
return UiPathChatVertex(
model_name=model,
temperature=temperature,
max_tokens=max_tokens,
agenthub_config=agenthub_config,
byo_connection_id=byo_connection_id,
output_version="v1",
**sampling_kwargs,
**kwargs,
)
case APIFlavor.VERTEX_ANTHROPIC_CLAUDE:
Expand Down Expand Up @@ -243,12 +260,16 @@ def get_chat_model(
vendor, api_flavor = _compute_vendor_and_api_flavor(model_info)
model_name: str = model_info.get("modelName", model)

effective_temperature: float | None = (
None if _should_skip_temperature(model_info) else temperature
)

match LLMProvider(vendor):
case LLMProvider.OPENAI:
return _create_openai_llm(
model_name,
api_flavor,
temperature,
effective_temperature,
max_tokens,
agenthub_config,
byo_connection_id,
Expand All @@ -258,7 +279,7 @@ def get_chat_model(
return _create_bedrock_llm(
model_name,
api_flavor,
temperature,
effective_temperature,
max_tokens,
agenthub_config,
byo_connection_id,
Expand All @@ -268,7 +289,7 @@ def get_chat_model(
return _create_vertex_llm(
model_name,
api_flavor,
temperature,
effective_temperature,
max_tokens,
agenthub_config,
byo_connection_id,
Expand Down
145 changes: 145 additions & 0 deletions tests/chat/test_chat_model_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
_API_FLAVOR_TO_PROVIDER,
_DEFAULT_API_FLAVOR,
_compute_vendor_and_api_flavor,
get_chat_model,
)
from uipath_langchain.chat.types import APIFlavor, LLMProvider

Expand Down Expand Up @@ -320,3 +321,147 @@ def test_default_flavors_map_back_to_same_provider(self):
f"Default flavor {default_flavor} for {provider} "
f"maps to {mapped_provider} instead"
)


class TestGetChatModelTemperatureGating:
"""End-to-end tests that call ``get_chat_model`` and assert how
``temperature`` is forwarded to the underlying LangChain chat class.

The gate is driven by discovery's ``modelDetails.shouldSkipTemperature``:
when True, ``temperature`` must be omitted from the constructor kwargs;
when False/absent, it must be passed through as-is.
"""

def test_opus_4_7_bedrock_converse_omits_temperature(self, mocker):
"""flag=True + Bedrock Converse: UiPathChatBedrockConverse must be
instantiated without a ``temperature`` kwarg."""
pytest.importorskip("langchain_aws")
mocker.patch(
"uipath_langchain.chat.chat_model_factory._get_model_info",
return_value={
"modelName": "anthropic.claude-opus-4-7",
"vendor": "AwsBedrock",
"apiFlavor": "AwsBedrockConverse",
"modelDetails": {"shouldSkipTemperature": True},
},
)
mock_cls = mocker.patch(
"uipath_langchain.chat.bedrock.UiPathChatBedrockConverse"
)

get_chat_model(
model="anthropic.claude-opus-4-7",
temperature=0.0,
max_tokens=4096,
agenthub_config="cfg",
)

_, kwargs = mock_cls.call_args
assert "temperature" not in kwargs

def test_sonnet_4_5_bedrock_converse_forwards_temperature(self, mocker):
"""flag=False: UiPathChatBedrockConverse receives the exact caller
temperature."""
pytest.importorskip("langchain_aws")
mocker.patch(
"uipath_langchain.chat.chat_model_factory._get_model_info",
return_value={
"modelName": "anthropic.claude-sonnet-4-5-20250929-v1:0",
"vendor": "AwsBedrock",
"apiFlavor": "AwsBedrockConverse",
"modelDetails": {"shouldSkipTemperature": False},
},
)
mock_cls = mocker.patch(
"uipath_langchain.chat.bedrock.UiPathChatBedrockConverse"
)

get_chat_model(
model="anthropic.claude-sonnet-4-5-20250929-v1:0",
temperature=0.7,
max_tokens=4096,
agenthub_config="cfg",
)

_, kwargs = mock_cls.call_args
assert kwargs.get("temperature") == 0.7

def test_gpt_openai_responses_forwards_temperature_when_flag_absent(self, mocker):
"""Older discovery payloads have ``modelDetails: null``; the gate
must default to not-skipping and UiPathChatOpenAI must receive the
caller temperature."""
pytest.importorskip("langchain_openai")
mocker.patch(
"uipath_langchain.chat.chat_model_factory._get_model_info",
return_value={
"modelName": "gpt-5-2025-08-07",
"vendor": "OpenAi",
"apiFlavor": "OpenAiResponses",
"modelDetails": None,
},
)
mock_cls = mocker.patch("uipath_langchain.chat.openai.UiPathChatOpenAI")

get_chat_model(
model="gpt-5-2025-08-07",
temperature=0.3,
max_tokens=2048,
agenthub_config="cfg",
)

_, kwargs = mock_cls.call_args
assert kwargs.get("temperature") == 0.3

def test_byom_custom_name_honors_discovery_flag(self, mocker):
"""BYOM display names don't match any known alias, but the discovery
flag still identifies the underlying model — the gate must use it
and the leaf client must be built without a temperature kwarg."""
pytest.importorskip("langchain_aws")
mocker.patch(
"uipath_langchain.chat.chat_model_factory._get_model_info",
return_value={
"modelName": "Custom BYOM Opus 4.7",
"vendor": "AwsBedrock",
"apiFlavor": "AwsBedrockConverse",
"modelDetails": {"shouldSkipTemperature": True},
},
)
mock_cls = mocker.patch(
"uipath_langchain.chat.bedrock.UiPathChatBedrockConverse"
)

get_chat_model(
model="Custom BYOM Opus 4.7",
temperature=0.7,
max_tokens=4096,
agenthub_config="cfg",
)

_, kwargs = mock_cls.call_args
assert "temperature" not in kwargs

def test_gemini_vertex_forwards_temperature(self, mocker):
"""Third vendor path: flag=False on a Vertex Gemini model must
forward the caller temperature to UiPathChatVertex."""
pytest.importorskip("langchain_google_genai")
pytest.importorskip("google.genai")
mocker.patch(
"uipath_langchain.chat.chat_model_factory._get_model_info",
return_value={
"modelName": "gemini-2.5-pro",
"vendor": "VertexAi",
"apiFlavor": "GeminiGenerateContent",
"modelDetails": {"shouldSkipTemperature": False},
},
)
mock_cls = mocker.patch("uipath_langchain.chat.vertex.UiPathChatVertex")

get_chat_model(
model="gemini-2.5-pro",
temperature=0.5,
max_tokens=2048,
agenthub_config="cfg",
)

_, kwargs = mock_cls.call_args
assert kwargs.get("temperature") == 0.5
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading