Skip to content

tool_factory

function_to_tool(func)

Converts a standard Python function into a Tool object. Wraps the function to handle JSON inputs/outputs automatically.

Source code in wintermute/ai/utils/tool_factory.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def function_to_tool(func: Callable[..., Any]) -> Tool:
    """
    Converts a standard Python function into a Tool object.
    Wraps the function to handle JSON inputs/outputs automatically.
    """
    func_name = func.__name__
    func_doc = func.__doc__ or "No description provided."

    # 1. GENERATE INPUT SCHEMA
    # Inspect arguments to build the Pydantic model
    type_hints = get_type_hints(func)
    input_fields = {}

    signature = inspect.signature(func)
    for param_name, param in signature.parameters.items():
        if param_name == "self":
            continue

        annotation = type_hints.get(param_name, Any)
        default = param.default

        if default == inspect.Parameter.empty:
            input_fields[param_name] = (annotation, ...)
        else:
            input_fields[param_name] = (annotation, default)

    InputModel = create_model(f"{func_name}_Input", **input_fields)  # type: ignore
    input_schema = cast(JSONObject, InputModel.model_json_schema())

    # 2. GENERATE OUTPUT SCHEMA
    # We inspect the return type.
    # If the function returns `int`, the tool output will be `{"result": int}`.
    return_annotation = type_hints.get("return", Any)

    # Create a dynamic model for the output to get a valid JSON Schema
    OutputModel = create_model(f"{func_name}_Output", result=(return_annotation, ...))
    output_schema = cast(JSONObject, OutputModel.model_json_schema())

    # 3. CREATE THE WRAPPER (The Handler)
    # This transforms the Tool's JSON input into the function's args,
    # and transforms the function's return value back into JSON.
    def adapter_handler(params: JSONObject) -> JSONObject:
        # Validate input using our generated model (optional but safe)
        validated_params = InputModel(**params)

        # Call the actual function with unpacked arguments
        result = func(**validated_params.model_dump())

        # Intercept oversized payloads (raw bytes, multi-KB strings) before
        # they reach the LLM. Tools writing firmware dumps can therefore just
        # `return raw_flash_bytes` and the descriptor is what the model sees.
        offloaded = _maybe_offload_payload(result, get_default_workspace())
        if offloaded is not None:
            return offloaded

        # Pack the result into a JSON object matching our output schema
        # Note: If your function already returns a dict, you might want logic here
        # to decide whether to wrap it in "result" or return as is.
        # For consistency, we always wrap strictly typed returns in "result".
        return {"result": result}

    # 4. INSTANTIATE TOOL
    return Tool(
        name=func_name,
        description=func_doc,
        input_schema=input_schema,
        output_schema=output_schema,
        handler=adapter_handler,
    )