Skip to content

Union types don't work? #55

Closed
Closed

Description

@jexp
from google.adk.agents import Agent
import random
from typing import Union

def dice(count: Union[int, str] = 6) -> int:
    "Rolls an n-sided dice. n is the parameter as string or int"
    return int(random.random() * int(count))

root_agent = Agent(
    name="chat_agent",
    model="gemini-2.0-flash-exp",
    instruction="""
    You are a friendly chat companion, answer the user's questions stay social.
    When asked about facts only answer for well known topics, 
    otherwise state that you don't know. Don't make up stuff!
    """,
    tools=[dice]
)
2025-04-11 00:41:15,206 - INFO - _client.py:1740 - HTTP Request: POST https://us-central1-aiplatform.googleapis.com/v1beta1/projects/vertex-ai-neo4j-extension/locations/us-central1/publishers/google/models/gemini-2.0-flash-exp:generateContent "HTTP/1.1 400 Bad Request"
2025-04-11 00:41:15,232 - ERROR - fast_api.py:616 - Error in event_generator: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Unable to submit request because `dice` functionDeclaration `parameters.count` schema specified other fields alongside any_of. When using any_of, it must be the only field set. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling', 'status': 'INVALID_ARGUMENT'}}
Traceback (most recent call last):
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/cli/fast_api.py", line 605, in event_generator
    async for event in runner.run_async(
    ...<8 lines>...
      yield f"data: {sse_event}\n\n"
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/runners.py", line 197, in run_async
    async for event in invocation_context.agent.run_async(invocation_context):
    ...<2 lines>...
      yield event
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/agents/base_agent.py", line 141, in run_async
    async for event in self._run_async_impl(ctx):
      yield event
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/agents/llm_agent.py", line 232, in _run_async_impl
    async for event in self._llm_flow.run_async(ctx):
      self.__maybe_save_output_to_state(event)
      yield event
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/flows/llm_flows/base_llm_flow.py", line 231, in run_async
    async for event in self._run_one_step_async(invocation_context):
      last_event = event
      yield event
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/flows/llm_flows/base_llm_flow.py", line 257, in _run_one_step_async
    async for llm_response in self._call_llm_async(
    ...<6 lines>...
        yield event
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/flows/llm_flows/base_llm_flow.py", line 470, in _call_llm_async
    async for llm_response in llm.generate_content_async(
    ...<16 lines>...
      yield llm_response
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/adk/models/google_llm.py", line 140, in generate_content_async
    response = await self.api_client.aio.models.generate_content(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<3 lines>...
    )
    ^
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/genai/models.py", line 6429, in generate_content
    response = await self._generate_content(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        model=model, contents=contents, config=config
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/genai/models.py", line 5431, in _generate_content
    response_dict = await self._api_client.async_request(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        'post', path, request_dict, http_options
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/genai/_api_client.py", line 708, in async_request
    result = await self._async_request(http_request=http_request, stream=False)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/genai/_api_client.py", line 652, in _async_request
    await errors.APIError.raise_for_async_response(response)
  File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/genai/errors.py", line 129, in raise_for_async_response
    raise ClientError(status_code, response_json, response)
google.genai.errors.ClientError: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Unable to submit request because `dice` functionDeclaration `parameters.count` schema specified other fields alongside any_of. When using any_of, it must be the only field set. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling', 'status': 'INVALID_ARGUMENT'}}
INFO:     127.0.0.1:57384 - "GET /apps/hello-agent/users/user/sessions/ee4e2bce-9416-43a9-a533-99fdb87da4e5 HTTP/1.1" 200 OK

Activity

  1. mattbeard0 commented on Apr 10, 2025

    @mattbeard0

    Inputs to functions should be more deterministic. This should work as follows:

    def dice(count: int = 6) -> int:
        "Rolls an n-sided dice. n is the parameter as string or int"
    
        if not isinstance(count, int):
             "Input to function should be an integer"
       
        return int(random.random() * count)
    
  2. jexp commented on Apr 11, 2025

    @jexp
    Author

    This was just a minimal example. There are other cases where you have not so easily resolved combinations

  3. hangfei commented on Apr 12, 2025

    @hangfei
    Collaborator

    Current model function calling generally do better with simpler types.

    @jexp could you share examples where union is necessary?

  4. bennnym commented on Apr 12, 2025

    @bennnym

    Is there any planned support for pydantic types @hangfei ?

  5. hangfei commented on Apr 14, 2025

    @hangfei
    Collaborator

    @bennnym could you share a code piece of the expected behaviour?

  6. self-assigned this
    on Apr 14, 2025
  7. built-by-as commented on Apr 15, 2025

    @built-by-as

    @hangfei parity with this would be greatly appreciated: https://openai.github.io/openai-agents-python/tools/#automatic-argument-and-docstring-parsing

    Pydantic models allow for calling functions with complex structs seamlessly

  8. boyangsvl commented on Apr 19, 2025

    @boyangsvl
    Collaborator

    Automatic parsing of argument and docstring is supported by ADK. You can use Pydantic models as arguments in function tools. We just don't support union types as it's a limitation of the model. I'm closing this issue for now.

  9. sandangel commented on May 4, 2025

    @sandangel

    Hi, I'm using simple type: str | None = None to mention this argument is optional, if user is not mentioning it then it should be null. Right now it's returning error. Is it possible to support that case? @boyangsvl

  10. mattbeard0 commented on May 4, 2025

    @mattbeard0

    Hi, I'm using simple type: str | None = None to mention this argument is optional, if user is not mentioning it then it should be null. Right now it's returning error. Is it possible to support that case? @boyangsvl

    Have tried: def random_function(input: Optional[str] = None) with from typing import Optional.

    Or a less optimal solution of within your tool prompting either a) forcing the LLM to input something there, or b) prompting the LLM to be "if you want the parameter to be blank, input "None" as the string", and writing if input == "None" then skip

  11. sandangel commented on May 4, 2025

    @sandangel

    Hi, in the company, we are using str | None as the new way in python to express Optional args and even have a ruff rule to convert Optional[str] to str | None. I think other framework like autogen already supported it. They also support Annotated type for add description to arguments.

    https://github.com/microsoft/autogen/blob/main/python/packages/autogen-core/src/autogen_core/tools/_function_tool.py#L65-L67

                async def get_stock_price(ticker: str, date: Annotated[str, "Date in YYYY/MM/DD"]) -> float:
                    # Simulates a stock price retrieval by returning a random float within a specified range.
                    return random.uniform(10, 200)

    Is there a reason why we can't support Union syntax for optional type? I believe in the schema for arguments there is a field called required and if it's optional type then we can just turn the str | None to required: false?

  12. sandangel commented on May 5, 2025

    @sandangel

    I also just tried Optional[str], then it generates an error:

    Invoking `docs_query_tool()` failed as the following mandatory input parameters are not present: query You could retry calling this tool, but it is IMPORTANT for you to provide all the mandatory parameters.
    

    This is my code:

    async def docs_query_tool(
        semantic_query: str,
        keyword_query: Optional[str],
        tool_context: ToolContext,
    ) -> str:
        pass
  13. jonigl commented on May 14, 2025

    @jonigl

    I’m running into the same problem when I hook ADK up to a few community MCP servers. Their tool schemas mark some parameters as Optional[...], and ADK forwards that straight to Vertex AI/Gemini as an anyOf union that includes "type": "null". Gemini then throws

    Unable to submit request because `x` functionDeclaration `parameters.x` schema specified other fields alongside any_of. When using any_of, it must be the only field set. 
    

    Sadly until ADK flattens those Optional fields to the Vertex-AI–style nullable: true form, we can’t integrate those MCP servers without manually rewriting the schema.

  14. jexp commented on May 26, 2025

    @jexp
    Author

    It's still happening, seems to be a shortcoming of the Gemini API itself, that ADK would have to wrap/adapt to?

    Trying to use an MCP server (that works well everywhere else)

      File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/genai/_api_client.py", line 733, in _async_request
        await errors.APIError.raise_for_async_response(response)
      File "/Users/mh/d/llm/adk-test/.venv/lib/python3.13/site-packages/google/genai/errors.py", line 129, in raise_for_async_response
        raise ClientError(status_code, response_json, response)
    google.genai.errors.ClientError: 400 INVALID_ARGUMENT. {'error': {'code': 400, 'message': 'Unable to submit request because `read_neo4j_cypher` functionDeclaration `parameters.params` schema specified other fields alongside any_of. When using any_of, it must be the only field set. Learn more: https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/function-calling', 'status': 'INVALID_ARGUMENT'}}
    
    from google.adk.agents import Agent
    from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StdioServerParameters
    import random
    import requests
    import base64
    import dotenv
    import os
    
    dotenv.load_dotenv("neo4j-memory.env")
    
    root_agent = Agent(
        name="memory_agent",
        model="gemini-2.0-flash-exp",
        description="""
        Agent to access conversational and fact memories as Memory entities with observations 
        and relationships stored in a knowledge graph.
        """,
        instruction="""
        You are an assistant with access to a graph database with 
        """,
        tools=[MCPToolset(
            connection_params=StdioServerParameters(
                command='uvx',
                args=[
                    "mcp-neo4j-cypher",
                ],
                env={ k: os.environ[k] for k in ["NEO4J_URI","NEO4J_USERNAME","NEO4J_PASSWORD"] }
            ),
            # optional tool_filter=['tool1', 'tool2']
        )]
    )
  15. jexp commented on May 26, 2025

    @jexp
    Author

    The source MCP server is using Optional for parameters.

        async def read_neo4j_cypher(
            query: str = Field(..., description="The Cypher query to execute."),
            params: Optional[dict[str, Any]] = Field(
                None, description="The parameters to pass to the Cypher query."
            ),
    
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions