Skip to content

example @function_tool with optional parameters #43

Not planned
@codefromthecrypt

Description

@codefromthecrypt
Contributor

I'm struggling to find a way to declare a function which has an optional parameter. Ideally, I want something like this. Let me know what's the way, if you don't mind!

@function_tool
def get_latest_elasticsearch_version(major_version: int | None = None) -> str:
    """Returns the latest GA version of Elasticsearch in "X.Y.Z" format.

    Args:
        major_version: Major version to filter by (e.g. 7, 8). Defaults to latest
    """

--snip--

Activity

rm-openai

rm-openai commented on Mar 12, 2025

@rm-openai
Collaborator

When you use @function_tool, it enforces "strict mode" (https://platform.openai.com/docs/guides/function-calling?api-mode=chat#strict-mode), which means params are required.

See code/output
@function_tool
def get_latest_elasticsearch_version(major_version: int | None = None) -> str:
    """Returns the latest GA version of Elasticsearch in "X.Y.Z" format.

    Args:
        major_version: Major version to filter by (e.g. 7, 8). Defaults to latest
    """
    return "test"

print(json.dumps(get_latest_elasticsearch_version.params_json_schema, indent=2))

prints:

{
  "properties": {
    "major_version": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
      "title": "Major Version"
    }
  },
  "title": "get_latest_elasticsearch_version_args",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "major_version"
  ]
}

If you specifically don't want strict mode, you can manually create a FunctionTool. Note that this is not recommended - strict mode makes JSON much more reliable. But here's an example of how you might do this, still leveraging the function_schema helper:

import json
from typing import Any

from agents import FunctionTool, RunContextWrapper
from agents.function_schema import function_schema


def get_latest_elasticsearch_version(major_version: int | None = None) -> str:
    """Returns the latest GA version of Elasticsearch in "X.Y.Z" format.

    Args:
        major_version: Major version to filter by (e.g. 7, 8). Defaults to latest
    """
    return "test"


schema = function_schema(get_latest_elasticsearch_version, strict_json_schema=False)


async def on_invoke_tool(ctx: RunContextWrapper[Any], input_json: str) -> str:
    parsed_json = json.loads(input_json)
    data = schema.params_pydantic_model(**parsed_json)
    args, kwargs = schema.to_call_args(data)
    return get_latest_elasticsearch_version(*args, **kwargs)


tool = FunctionTool(
    name=schema.name,
    description=schema.description or "",
    params_json_schema=schema.params_json_schema,
    on_invoke_tool=on_invoke_tool,
)

print(json.dumps(tool.params_json_schema, indent=2))

output:

{
  "properties": {
    "major_version": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
      "title": "Major Version"
    }
  },
  "title": "get_latest_elasticsearch_version_args",
  "type": "object"
}
codefromthecrypt

codefromthecrypt commented on Mar 12, 2025

@codefromthecrypt
ContributorAuthor

@rm-openai thanks for the workaround!

Since other genai frameworks support optional parameters (via supplying defaults), is it possible to convert this to a feature request? I think that even if at the moment we can work around this, it would be more elegant to be able to express a function naturally. This will make it easier to port code elegantly. WDYT?

rm-openai

rm-openai commented on Mar 12, 2025

@rm-openai
Collaborator

Yes, makes sense. I think the most sensible way to do this is by adding a strict_mode: bool = True to function_tool(). Will try to get it soon, and leave it open in case someone else is interested.

Jai0401

Jai0401 commented on Mar 12, 2025

@Jai0401
Contributor

Hey @rm-openai, I’d love to work on implementing this feature. Would you be open to a PR for adding strict_mode: bool = True to @function_tool()? If so, I can start working on it and align with any specific guidelines you have in mind.

rm-openai

rm-openai commented on Mar 12, 2025

@rm-openai
Collaborator

Sure thing. Should be straightforward - add strict_mode as a param, document why its not a good idea to set to False, thread it through to function_schema/FunctionTool, and add tests. Thanks for taking this on!

codefromthecrypt

codefromthecrypt commented on Mar 12, 2025

@codefromthecrypt
ContributorAuthor

heh cool, yeah I had WIP but go for it @Jai0401 I'll help you review.

edge cases are multiple optional parameters of different types. e.g. x: int = 42, x: string = "hello", and of course the motivating optional one x: int | None = None.

also independently test the things you need to change to support this (e.g. in function_schema.py)

Have fun!

rm-openai

rm-openai commented on Mar 12, 2025

@rm-openai
Collaborator

@codefromthecrypt - I realized I misread the original schema. This should actually work out of the box with the current SDK. Did you actually run into an issue?

added
needs-more-infoWaiting for a reply/more info from the author
and removed
enhancementNew feature or request
on Mar 12, 2025
codefromthecrypt

codefromthecrypt commented on Mar 13, 2025

@codefromthecrypt
ContributorAuthor

@rm-openai yep. my original example in the desc creates the following API call. @Jai0401 maybe you can add a unit test about the schema created in your PR, though the tests you have prove the same point I think.

So in the current code, a follow-up to this fails as it sees major_version as a required field. I guess it is expecting the LLM to pass literally null instead of leave it out.

{
  "messages": [
    {
      "role": "user",
      "content": "What is the latest version of Elasticsearch?"
    }
  ],
  "model": "qwen2.5:0.5b",
  "stream": false,
  "temperature": 0,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_latest_elasticsearch_version",
        "description": "Returns the latest GA version of Elasticsearch in \"X.Y.Z\" format.",
        "parameters": {
          "properties": {
            "major_version": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
              "title": "Major Version"
            }
          },
          "required": [
            "major_version"
          ],
          "title": "get_latest_elasticsearch_version_args",
          "type": "object",
          "additionalProperties": false
        }
      }
    }
  ]
}

I'm expecting more like some other frameworks, where when it is optional, the wrapped type is used without being required (ideal IMHO), or in worst case keep it defined as an optional, but don't mark it required (e.g. pydantic AI interpretation of the same signature and docstring)

{
  "messages": [
    {
      "role": "user",
      "content": "What is the latest version of Elasticsearch 8?"
    }
  ],
  "model": "qwen2.5:3b",
  "n": 1,
  "stream": false,
  "temperature": 0,
  "tool_choice": "auto",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_latest_elasticsearch_version",
        "description": "Returns the latest GA version of Elasticsearch in \"X.Y.Z\" format.",
        "parameters": {
          "properties": {
            "major_version": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Major version to filter by (e.g. 7, 8). Defaults to latest",
              "title": "Major Version"
            }
          },
          "type": "object",
          "additionalProperties": false
        }
      }
    }
  ]
}

Make sense?

rm-openai

rm-openai commented on Mar 13, 2025

@rm-openai
Collaborator

Is there any reason you'd prefer to not use strict mode? The LLM can indeed pass null as you noted. And strict mode basically guarantees valid JSON, which is a big deal.

codefromthecrypt

codefromthecrypt commented on Mar 13, 2025

@codefromthecrypt
ContributorAuthor

The problem is that the LLM isn't passing null. Maybe I worded incorrectly. This only works if I pass a version in my question. If I don't, it breaks out asking me to supply one.

14 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @codefromthecrypt@Jai0401@HG2407@rm-openai

      Issue actions

        example @function_tool with optional parameters · Issue #43 · openai/openai-agents-python