Skip to content
Open
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 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.59"
version = "0.1.60"
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,
api_version: str = NORMALIZED_API_VERSION,
Expand Down Expand Up @@ -583,10 +583,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.73"
version = "2.10.74"
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.8, <0.6.0",
"uipath-runtime>=0.10.1, <0.11.0",
"uipath-platform>=0.1.59, <0.2.0",
"uipath-platform>=0.1.60, <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