Skip to content

byllm: dict return types fail validation in Tool.parse_arguments with ReAct tools #5243

@udithishanka

Description

@udithishanka

Bug

When using dict[str, str | None] (or any dict type) as the return type of a by llm(tools=([...])) function, Pydantic validation fails intermittently with:

1 validation error for dict[str,nullable[str]]
schema_dict_wrapper
  Input should be a valid string [type=string_type, input_value=[{'key': 'assets', 'value...d: BDO, Balance: 500$'}], input_type=list]

Root Cause

_convert_dict_to_schema wraps dict types into a schema_dict_wrapper array format for the LLM API. The LLM returns the dict in this wrapped format.

The response parsing path (json_to_instance in schema.impl.jac) correctly calls _decode_dict to unwrap it before Pydantic validation. However, Tool.parse_arguments in types.impl/tool.impl.jac calls TypeAdapter(arg_type).validate_python(arg_json) directly on the wrapped JSON without first calling _decode_dict.

This affects the finish_tool in ReAct flows — when the LLM calls finish_tool(final_output={"schema_dict_wrapper": [...]}), parse_arguments tries to validate the wrapped format against dict[str, str | None], and Pydantic rejects the array value as not being a string.

Suggested Fix

In byllm/types.impl/tool.impl.jac, Tool.parse_arguments:

impl Tool.parse_arguments(args_json: dict) -> dict {
    args = {};
    annotations: dict = {};
    try {
        annotations = self.func.__annotations__;
    } except AttributeError {
        annotations = get_type_hints(self.func);
    }
    for (arg_name, arg_json) in args_json.items() {
        if arg_type := annotations.get(arg_name) {
            if isinstance(arg_json, dict) {
                arg_json = _decode_dict(arg_json);  # <-- decode before validation
            }
            args[arg_name] = TypeAdapter(arg_type).validate_python(arg_json);
        }
    }
    return args;
}

And add _decode_dict to the import in byllm/types.jac:

import from .schema { tool_to_schema, _decode_dict }

Workaround

Use an obj instead of dict for the return type:

obj AssistantResponse {
    has message: str,
        html: str | None = None;
}

def process_request(query: str, history: str) -> AssistantResponse by llm(tools=([...]));

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working as expected.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions