Skip to content

[PY] Nullable(Optional) Field cannot be handled #2975

@msk-psp

Description

@msk-psp

Describe the bug
I don't know is this intended behavior not to use optional field for response model. but it seems bug since genkit for node.js supports optional field.

If the response model includes nullable field. an error occurs while converting the model to supports OpenAI spec in following function.

google.genai._transformers.process_schema._recurse

def _recurse(sub_schema: dict[str, Any]) -> dict[str, Any]:
    """Returns the processed `sub_schema`, resolving its '$ref' if any."""
    if (ref := sub_schema.pop('$ref', None)) is not None:  ## <--- if sub_schema is None then "AttributeError: 'NoneType' object has no attribute 'pop'" occured.
      sub_schema = defs[ref.split('defs/')[-1]]
    process_schema(sub_schema, client, defs, order_properties=order_properties)
    return sub_schema

because below function changes the Optional field into None which the type is anyOf
genkit.plugins.google_genai.models.gemini.GenimiModel._convert_schema_property

def _convert_schema_property(
        self, input_schema: dict[str, Any], defs: dict[str, Any] | None = None
    ) -> genai_types.Schema | None:
        """Sanitizes a schema to be compatible with Gemini API.

        Args:
            input_schema: A dictionary with input parameters
            defs: Dictionary with definitions. Optional.

        Returns:
            Schema or None
        """
        if input_schema is None or 'type' not in input_schema: # < --- this line changes the filed to None 
            return None

        if defs is None:
            defs = input_schema.get('$defs') if '$defs' in input_schema else {}

        schema = genai_types.Schema()
        if input_schema.get('description'):
            schema.description = input_schema['description']

        if 'required' in input_schema:
            schema.required = input_schema['required']

        if 'type' in input_schema:
            schema_type = genai_types.Type(input_schema['type'])
            schema.type = schema_type

            if 'enum' in input_schema:
                schema.enum = input_schema['enum']

            if schema_type == genai_types.Type.ARRAY:
                schema.items = self._convert_schema_property(input_schema['items'], defs)

            if schema_type == genai_types.Type.OBJECT:
                schema.properties = {}
                properties = input_schema['properties']
                for key in properties:
                    if isinstance(properties[key], dict) and '$ref' in properties[key]:
                        ref_tokens = properties[key]['$ref'].split('/')
                        if ref_tokens[2] not in defs:
                            raise ValueError(f'Failed to resolve schema for {ref_tokens[2]}')
                        resolved_schema = self._convert_schema_property(defs[ref_tokens[2]], defs)
                        schema.properties[key] = resolved_schema

                        if 'description' in properties[key]:
                            schema.properties[key].description = properties[key]['description']
                    else:
                        nested_schema = self._convert_schema_property(properties[key], defs)
                        schema.properties[key] = nested_schema

        return schema

To Reproduce
Give the response model includes nullable Field for chat completion that returns structured output.

Expected behavior
The nullable field successfully converted to OpenAI supports field by handle_null_fields function.

Screenshots
If applicable, add screenshots to help explain your problem.

Runtime (please complete the following information):

  • OS: Apple M1 Sequoia 15.4.1
  • "genkit>=0.4.0",
  • "genkit-plugin-google-genai>=0.4.0",

** Python version

  • Python 3.12.9

Additional context

import json
from pydantic import BaseModel, Field
from genkit.ai import Genkit
from genkit.plugins.google_genai import GoogleAI
from typing import Optional

from models.eng_kor_dictionary import DictionaryEntry
from utils.dictionary import get_lemmatized_word

ai = Genkit(
    plugins=[GoogleAI()],
    model='googleai/gemini-2.0-flash',
)

class RpgCharacter(BaseModel):
    name: str = Field(description='name of the character')
    back_story: str = Field(description='back story')
    abilities: Optional[list[str]] = Field(description='list of abilities (3-4)')

@ai.flow()
async def generate_character(name: str):
    result = await ai.generate(
        prompt=f'generate an RPG character named {name}',
        output_schema=RpgCharacter,
    )
    return result.output

async def main() -> None:
    print(json.dumps(await generate_character('Goblorb'), indent=2))
(-) (base) ---@--- functions %  cd /Users/---/---/---/functions ; /usr/bin/env /Users/---/
.venv/bin/python /Users/---/.cursor/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bu
ndled/libs/debugpy/adapter/../../debugpy/launcher 53232 -- ai_app.py 
Traceback (most recent call last):
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/core/action/_action.py", line 519, in async_tracing_wrapper
    output = await afn(input, ctx)
             ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/plugins/google_genai/models/gemini.py", line 683, in generate
    response = await self._generate(
               ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/plugins/google_genai/models/gemini.py", line 719, in _generate
    response = await self._client.aio.models.generate_content(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/google/genai/models.py", line 7124, in generate_content
    response = await self._generate_content(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/google/genai/models.py", line 6094, in _generate_content
    request_dict = _GenerateContentParameters_to_mldev(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/google/genai/models.py", line 812, in _GenerateContentParameters_to_mldev
    _GenerateContentConfig_to_mldev(
  File "/Users/---/.venv/lib/python3.12/site-packages/google/genai/models.py", line 691, in _GenerateContentConfig_to_mldev
    t.t_schema(api_client, getv(from_object, ['response_schema'])),
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/google/genai/_transformers.py", line 824, in t_schema
    process_schema(schema, client)
  File "/Users/---/.venv/lib/python3.12/site-packages/google/genai/_transformers.py", line 762, in process_schema
    properties[name] = _recurse(sub_schema)
                       ^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/google/genai/_transformers.py", line 735, in _recurse
    if (ref := sub_schema.pop('$ref', None)) is not None:
               ^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'pop'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/core/action/_action.py", line 517, in async_tracing_wrapper
    output = await afn(input)
             ^^^^^^^^^^^^^^^^
  File "ai_app.py", line 22, in generate_character
    result = await ai.generate(
             ^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/ai/_aio.py", line 162, in generate
    return await generate_action(
           ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/blocks/generate.py", line 231, in generate_action
    model_response = await dispatch(
                     ^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/blocks/generate.py", line 209, in dispatch
    await model.arun(
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/core/action/_action.py", line 332, in arun
    return await self._afn(
           ^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/core/action/_action.py", line 523, in async_tracing_wrapper
    raise GenkitError(
genkit.core.error.GenkitError: None: Error while running action googleai/gemini-2.0-flash

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/Cellar/[email protected]/3.12.9/Frameworks/Python.framework/Versions/3.12/lib/python3.12/runpy.py", line 198, in _run_module_as_main
    return _run_code(code, main_globals, None,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/[email protected]/3.12.9/Frameworks/Python.framework/Versions/3.12/lib/python3.12/runpy.py", line 88, in _run_code
    exec(code, run_globals)
  File "/Users/---/.cursor/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/__main__.py", line 39, in <module>
    cli.main()
  File "/Users/---/.cursor/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 430, in main
    run()
  File "/Users/---/.cursor/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/adapter/../../debugpy/launcher/../../debugpy/../debugpy/server/cli.py", line 284, in run_file
    runpy.run_path(target, run_name="__main__")
  File "/Users/---/.cursor/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 321, in run_path
    return _run_module_code(code, init_globals, run_name,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.cursor/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 135, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/Users/---/.cursor/extensions/ms-python.debugpy-2024.6.0-darwin-arm64/bundled/libs/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_runpy.py", line 124, in _run_code
    exec(code, run_globals)
  File "ai_app.py", line 50, in <module>
    ai.run_main(main())
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/ai/_base.py", line 89, in run_main
    result = asyncio.run(coro)
             ^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/[email protected]/3.12.9/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/[email protected]/3.12.9/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/Cellar/[email protected]/3.12.9/Frameworks/Python.framework/Versions/3.12/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "ai_app.py", line 29, in main
    print(json.dumps(await generate_character('Goblorb'), indent=2))
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/ai/_registry.py", line 148, in async_wrapper
    return (await action.arun(*args, **kwargs)).response
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/core/action/_action.py", line 332, in arun
    return await self._afn(
           ^^^^^^^^^^^^^^^^
  File "/Users/---/.venv/lib/python3.12/site-packages/genkit/core/action/_action.py", line 523, in async_tracing_wrapper
    raise GenkitError(
genkit.core.error.GenkitError: None: Error while running action generate_character

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpythonPython

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions