Skip to content

Commit 0d98d7a

Browse files
Saran33mdrxyCopilot
authored
fix(genai): preserve field descriptions and enum values for Union types with None in function schemas (#1092)
<!-- # Thank you for contributing to LangChain-google! --> <!-- ## Checklist for PR Creation - [ ] PR Title: "[package]: [brief description]" - Where "package" is genai, vertexai, or community - Use "docs: ..." for purely docs changes, "templates: ..." for template changes, "infra: ..." for CI changes - Example: "community: add foobar LLM" - [ ] PR Description and Relevant issues: - Description of the change - Relevant issues (if applicable) - Any dependencies required for this change - [ ] Add Tests and Docs: - If adding a new integration: 1. Include a test for the integration (preferably unit tests that do not rely on network access) 2. Add an example notebook showing its use (place in the `docs/docs/integrations` directory) - [ ] Lint and Test: - Run `make format`, `make lint`, and `make test` from the root of the package(s) you've modified - See contribution guidelines for more: https://github.com/langchain-ai/langchain-google/blob/main/README.md#contribute-code --> <!-- ## Additional guidelines - [ ] PR title and description are appropriate - [ ] Necessary tests and documentation have been added - [ ] Lint and tests pass successfully - [ ] The following additional guidelines are adhered to: - Optional dependencies are imported within functions - No unnecessary dependencies added to pyproject.toml files (except those required for unit tests) - PR doesn't touch more than one package - Changes are backwards compatible --> ## PR Description <!-- e.g. "Implement user authentication feature" --> - When using ChatGoogleGenerativeAI with tools that have parameters typed as unions with None (e.g., list[str] | None), the field descriptions and enum values are not being passed to the Gemini model. This occurs because the schema conversion process loses critical field properties when handling anyOf JSON schema structures. ## Relevant issues <!-- e.g. "Fixes #000" --> Fixes: #1091 ## Type <!-- Select the type of Pull Request --> <!-- Keep only the necessary ones --> 🐛 Bug Fix ## Changes(optional) <!-- List of changes --> - **Preserve field descriptions**: Preserve description before any schema manipulation. Maintain existing behaviour of preferring the description from the filtered schema, but fall back to original description if not present. - **Preserve enum values**: When handling anyOf schemas for nullable union types, preserve original enum and items properties that would otherwise be lost during schema filtering. - **Enhanced items schema processing**: Added enum preservation in `_get_items_from_schema` to ensure array item constraints (like Literal types) are properly passed to Gemini. - **Extended schema filtering**: Added handling for all anyOf types (not just arrays/objects) to preserve enums in nullable string literals like `Optional[Literal["A", "B", "C"]]`. This fix ensures that field descriptions, enum constraints, and array item specifications are properly preserved and passed to Google Gemini models when using Union types with None, enabling proper function calling with constrained parameters. ## Testing(optional) <!-- Test procedure --> - **Unit Tests**: All unit tests pass, including specific `test_function_utils.py` tests that validate schema preservation - **Integration Tests**: Previously failing CI tests now pass: - `test_tool_calling_async` - (was failing in initial commit due to missing enum constraints) - `test_structured_output[typeddict]` - (was failing in initial commit due to lost field descriptions) - **Schema Validation**: Verified enum preservation for `Optional[Literal["A", "B", "C"]]` types - **Backward Compatibility**: Confirmed all existing functionality remains intact <!-- Test result --> - **Before fix**: Descriptions and enum values were lost during anyOf schema processing, resulting in unconstrained parameters and failed tool calls - **After fix**: Descriptions and enum values are properly preserved for all nullable union types, enabling correct function calling with constrained parameters ## Note(optional) <!-- Information about the errors fixed by PR --> The original issue only affected nullable union types with specific constraints (enums, literals). Regular non-nullable fields and simple nullable fields without constraints were unaffected. This fix specifically targets the anyOf schema processing logic to ensure all field metadata is preserved during schema transformation. <!-- Remaining issue or something --> <!-- Other information about PR --> --------- Co-authored-by: Mason Daugherty <[email protected]> Co-authored-by: Mason Daugherty <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent c634a61 commit 0d98d7a

File tree

1 file changed

+30
-3
lines changed

1 file changed

+30
-3
lines changed

libs/genai/langchain_google_genai/_function_utils.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,10 @@ def _get_properties_from_schema(schema: Dict) -> Dict[str, Any]:
330330
continue
331331
properties_item: Dict[str, Union[str, int, Dict, List]] = {}
332332

333-
# Get description from original schema before any modifications
334-
description = v.get("description")
333+
# Preserve description and other schema properties before manipulation
334+
original_description = v.get("description")
335+
original_enum = v.get("enum")
336+
original_items = v.get("items")
335337

336338
if v.get("anyOf") and all(
337339
anyOf_type.get("type") != "null" for anyOf_type in v.get("anyOf", [])
@@ -354,11 +356,34 @@ def _get_properties_from_schema(schema: Dict) -> Dict[str, Any]:
354356
if any_of_types and item_type_ in [glm.Type.ARRAY, glm.Type.OBJECT]:
355357
json_type_ = "array" if item_type_ == glm.Type.ARRAY else "object"
356358
# Use Index -1 for consistency with `_get_nullable_type_from_schema`
357-
v = [val for val in any_of_types if val.get("type") == json_type_][-1]
359+
filtered_schema = [
360+
val for val in any_of_types if val.get("type") == json_type_
361+
][-1]
362+
# Merge filtered schema with original properties to preserve enum/items
363+
v = filtered_schema.copy()
364+
if original_enum and not v.get("enum"):
365+
v["enum"] = original_enum
366+
if original_items and not v.get("items"):
367+
v["items"] = original_items
368+
elif any_of_types:
369+
# For other types (like strings with enums), find the non-null schema
370+
# and preserve enum/items from the original anyOf structure
371+
non_null_schemas = [
372+
val for val in any_of_types if val.get("type") != "null"
373+
]
374+
if non_null_schemas:
375+
filtered_schema = non_null_schemas[-1]
376+
v = filtered_schema.copy()
377+
if original_enum and not v.get("enum"):
378+
v["enum"] = original_enum
379+
if original_items and not v.get("items"):
380+
v["items"] = original_items
358381

359382
if v.get("enum"):
360383
properties_item["enum"] = v["enum"]
361384

385+
# Prefer description from the filtered schema, fall back to original
386+
description = v.get("description") or original_description
362387
if description and isinstance(description, str):
363388
properties_item["description"] = description
364389

@@ -415,6 +440,8 @@ def _get_items_from_schema(schema: Union[Dict, List, str]) -> Dict[str, Any]:
415440
items["description"] = (
416441
schema.get("description") or schema.get("title") or ""
417442
)
443+
if "enum" in schema:
444+
items["enum"] = schema["enum"]
418445
if _is_nullable_schema(schema):
419446
items["nullable"] = True
420447
if "required" in schema:

0 commit comments

Comments
 (0)