Skip to content
Merged
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.1.62"
version = "0.1.63"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ async def chat_completions(
presence_penalty: float = 0,
top_p: float | None = 1,
top_k: int | None = None,
tools: list[ToolDefinition] | None = None,
tools: list[ToolDefinition | dict[str, Any]] | None = None,
tool_choice: ToolChoice | None = None,
response_format: dict[str, Any] | type[BaseModel] | None = None,
Comment on lines 401 to 406
api_version: str = NORMALIZED_API_VERSION,
Expand Down Expand Up @@ -436,9 +436,11 @@ async def chat_completions(
Controls diversity by considering only the top p probability mass. Defaults to 1.
top_k (int, optional): Nucleus sampling parameter.
Controls diversity by considering only the top k most probable tokens. Defaults to None.
tools (Optional[List[ToolDefinition]], optional): List of tool definitions that the
model can call. Tools enable the model to perform actions or retrieve information
beyond text generation. Defaults to None.
tools (Optional[List[ToolDefinition | dict]], optional): List of tool definitions
that the model can call. Tools enable the model to perform actions or retrieve
information beyond text generation. A tool given as a dict must already be in
UiPath wire format and is forwarded unchanged, which allows arbitrary nested
JSON schemas in its parameters. Defaults to None.
tool_choice (Optional[ToolChoice], optional): Controls which tools the model can call.
Can be "auto" (model decides), "none" (no tools), or a specific tool choice.
Defaults to None.
Expand Down Expand Up @@ -583,10 +585,15 @@ class Country(BaseModel):
# Use provided dictionary format directly
request_body["response_format"] = response_format

# Add tools if provided - convert to UiPath format
# Add tools if provided. A tool already in UiPath wire format (a dict) is
# passed through unchanged so callers can supply an arbitrary JSON schema
# for the parameters; ToolDefinition objects are converted as before.
if tools:
request_body["tools"] = [
self._convert_tool_to_uipath_format(tool) for tool in tools
tool
if isinstance(tool, dict)
else self._convert_tool_to_uipath_format(tool)
for tool in tools
]

# Handle tool_choice
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from uipath.platform.chat import (
AutoToolChoice,
ChatModels,
RequiredToolChoice,
SpecificToolChoice,
ToolDefinition,
ToolFunctionDefinition,
Expand Down Expand Up @@ -369,6 +370,87 @@ async def test_tool_call_required_mocked(self, mock_request, llm_service):
assert result.choices[0].message.tool_calls[0].arguments["name"] == "John"
assert result.choices[0].message.tool_calls[0].arguments["password"] == "1234"

@pytest.mark.asyncio
@patch.object(UiPathLlmChatService, "request_async")
async def test_raw_dict_tool_passthrough_mocked(self, mock_request, llm_service):
"""A tool supplied as a raw dict is sent unchanged, preserving nested schema.

ToolDefinition's converter only emits flat properties, so callers that need
an arbitrary nested JSON schema (e.g. the eval mockers) pass the tool as a
dict already in UiPath wire format. It must reach the gateway verbatim.
"""
mock_response = MagicMock()
mock_response.json.return_value = {
"id": "chatcmpl-raw",
"object": "chat.completion",
"created": 1677858242,
"model": "gpt-4o-mini-2024-07-18",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_raw",
"name": "submit_tool_response",
"arguments": {"response": {"items": [{"sku": "A1"}]}},
}
],
},
"finish_reason": "tool_calls",
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
"cache_read_input_tokens": None,
},
}
mock_request.return_value = mock_response

nested_tool = {
"name": "submit_tool_response",
"description": "Return the simulated response matching the schema.",
"parameters": {
"type": "object",
"properties": {
"response": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {"sku": {"type": "string"}},
},
}
},
}
},
"required": ["response"],
},
}

result = await llm_service.chat_completions(
messages=[{"role": "user", "content": "go"}],
model=ChatModels.gpt_4_1_mini_2025_04_14,
tools=[nested_tool],
tool_choice=RequiredToolChoice(),
)

mock_request.assert_called_once()
_, kwargs = mock_request.call_args
body = kwargs["json"]
# The dict tool is forwarded byte-for-byte, nested array schema intact.
assert body["tools"] == [nested_tool]
assert body["tool_choice"] == {"type": "required"}
assert result.choices[0].message.tool_calls[0].arguments == {
"response": {"items": [{"sku": "A1"}]}
}

@pytest.mark.asyncio
@patch.object(UiPathLlmChatService, "request_async")
async def test_chat_with_conversation_history_mocked(
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-platform/uv.lock

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

4 changes: 2 additions & 2 deletions packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[project]
name = "uipath"
version = "2.10.80"
version = "2.10.81"
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.5.17, <0.6.0",
"uipath-runtime>=0.11.0, <0.12.0",
"uipath-platform>=0.1.60, <0.2.0",
"uipath-platform>=0.1.63, <0.2.0",
"click>=8.3.1",
"httpx>=0.28.1",
"pyjwt>=2.10.1",
Expand Down
28 changes: 8 additions & 20 deletions packages/uipath/src/uipath/eval/mocks/_input_mocker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .._execution_context import eval_set_run_id_context
from ._mock_context import cache_manager_context
from ._mocker import UiPathInputMockingError
from ._structured_output import generate_structured_output
from ._types import (
InputMockingStrategy,
)
Expand Down Expand Up @@ -105,15 +106,6 @@ async def generate_llm_input(

prompt = get_input_mocking_prompt(**prompt_generation_args)

response_format = {
"type": "json_schema",
"json_schema": {
"name": "agent_input",
"strict": False,
"schema": input_schema,
},
}

model_parameters = mocking_strategy.model if mocking_strategy else None
completion_kwargs = (
model_parameters.model_dump(by_alias=False, exclude_none=True)
Expand All @@ -128,7 +120,7 @@ async def generate_llm_input(

if cache_manager is not None:
cache_key_data = {
"response_format": response_format,
"input_schema": input_schema,
"completion_kwargs": completion_kwargs,
"prompt_generation_args": prompt_generation_args,
}
Expand All @@ -142,15 +134,15 @@ async def generate_llm_input(
if cached_response is not None:
return cached_response

response = await llm.chat_completions(
result = await generate_structured_output(
llm,
[{"role": "user", "content": prompt}],
response_format=response_format,
**completion_kwargs,
schema=input_schema,
response_format_name="agent_input",
description="Return the simulated agent input matching the required schema.",
completion_kwargs=completion_kwargs,
)

generated_input_str = response.choices[0].message.content
result = json.loads(generated_input_str)

if cache_manager is not None:
cache_manager.set(
mocker_type="input_mocker",
Expand All @@ -160,10 +152,6 @@ async def generate_llm_input(
)

return result
except json.JSONDecodeError as e:
raise UiPathInputMockingError(
f"Failed to parse LLM response as JSON: {str(e)}"
) from e
except UiPathInputMockingError:
raise
except Exception as e:
Expand Down
34 changes: 14 additions & 20 deletions packages/uipath/src/uipath/eval/mocks/_llm_mocker.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
UiPathMockResponseGenerationError,
UiPathNoMockFoundError,
)
from ._structured_output import generate_structured_output
from ._types import (
ExampleCall,
LLMMockingStrategy,
Expand Down Expand Up @@ -125,14 +126,7 @@ async def response(
"output_schema", TypeAdapter(return_type).json_schema()
)

response_format = {
"type": "json_schema",
"json_schema": {
"name": "OutputSchema",
"strict": False,
"schema": _cleanup_schema(output_schema),
},
}
cleaned_schema = _cleanup_schema(output_schema)
try:
# Safely pull examples from params.
example_calls = params.get("example_calls", [])
Expand Down Expand Up @@ -197,7 +191,7 @@ async def response(
formatted_prompt = PROMPT.format(**prompt_generation_args)

cache_key_data = {
"response_format": response_format,
"output_schema": cleaned_schema,
"completion_kwargs": completion_kwargs,
"prompt_generation_args": prompt_generation_args,
}
Expand All @@ -213,17 +207,17 @@ async def response(
if cached_response is not None:
return cached_response

response = await llm.chat_completions(
[
{
"role": "user",
"content": formatted_prompt,
},
],
response_format=response_format,
**completion_kwargs,
result = await generate_structured_output(
llm,
[{"role": "user", "content": formatted_prompt}],
schema=cleaned_schema,
response_format_name="OutputSchema",
description=(
"Return the simulated response for tool "
f"'{function_name}' matching the required schema."
),
completion_kwargs=completion_kwargs,
)
result = json.loads(response.choices[0].message.content)

if cache_manager is not None:
cache_manager.set(
Expand All @@ -235,7 +229,7 @@ async def response(

return result
except Exception as e:
raise UiPathMockResponseGenerationError() from e
raise UiPathMockResponseGenerationError(str(e)) from e
else:
raise UiPathNoMockFoundError(f"Method '{function_name}' is not simulated.")

Expand Down
Loading
Loading