fix(google-genai): preserve declared property order in schemas (#5839)
## Issue Closes #5838 ## Change `GoogleGenAiToolMapper.convertToGoogleSchema` copied a `JsonObjectSchema`'s properties into a `HashMap` and never set Gemini's `propertyOrdering`. `JsonObjectSchema` keeps the declared order (backed by a `LinkedHashMap`), but the copy lost it, so the generated schema's field order was effectively arbitrary. Since Gemini uses `propertyOrdering` to decide field emission order, structured-output responses and function-call arguments could come back with fields in a non-deterministic order. Fix: - copy properties into a `LinkedHashMap` to keep the declared order; - set `propertyOrdering` to the property keys for objects with more than one property. This matches the official google-genai Python SDK (`_transformers.py` sets `property_ordering = list(properties.keys())` for objects with >1 property) and the Vertex module in this repo (`SchemaHelper` already uses a `LinkedHashMap`). The mapper is shared by function-call parameter schemas and `responseSchema`, so both are fixed. The change is additive: no public API change (revapi clean). ## Tests Added `should_preserve_declared_property_order_and_set_property_ordering` and `should_set_property_ordering_on_nested_object` to `GoogleGenAiToolMapperTest`. Both fail on `main` (the first because the `HashMap` reorders the keys, the second because `propertyOrdering` is absent) and pass with the fix. ``` mvn -pl langchain4j-google-genai verify -DskipITs # 133 unit tests, 0 failures; spotless + revapi green mvn -pl langchain4j-core,langchain4j test # core 1270, 0 failures; main green ``` Both touched paths are also covered end-to-end by the module's live ITs that run in CI: `GoogleGenAiAiServiceWithToolsIT` (function calling) and `GoogleGenAiAiServiceWithJsonSchemaIT` (structured output). ## General checklist - [x] There are no breaking changes (API, behaviour) - [x] I have added unit and/or integration tests for my change - [x] The tests cover both positive and negative cases - [x] I have manually run all the unit and integration tests in the module I have added/changed, and they are all green - [x] I have manually run all the unit and integration tests in the core and main modules, and they are all green - [ ] I have added/updated the documentation - [ ] I have added an example in the examples repo (only for "big" features) - [ ] I have added/updated Spring Boot starter(s) (if applicable)
This commit is contained in:
parent
e073780617
commit
98a45a1926
|
|
@ -16,8 +16,9 @@ import dev.langchain4j.model.chat.request.json.JsonNumberSchema;
|
|||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
|
||||
import dev.langchain4j.model.chat.request.json.JsonStringSchema;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ class GoogleGenAiToolMapper {
|
|||
}
|
||||
|
||||
if (element instanceof JsonObjectSchema objectSchema) {
|
||||
Map<String, Schema> properties = new HashMap<>();
|
||||
Map<String, Schema> properties = new LinkedHashMap<>();
|
||||
if (objectSchema.properties() != null) {
|
||||
objectSchema
|
||||
.properties()
|
||||
|
|
@ -61,12 +62,15 @@ class GoogleGenAiToolMapper {
|
|||
properties.put(key, convertToGoogleSchema(value)));
|
||||
}
|
||||
|
||||
return Schema.builder()
|
||||
Schema.Builder builder = Schema.builder()
|
||||
.type(Type.Known.OBJECT)
|
||||
.properties(properties)
|
||||
.required(getOrDefault(objectSchema.required(), Collections.emptyList()))
|
||||
.description(getOrDefault(objectSchema.description(), ""))
|
||||
.build();
|
||||
.description(getOrDefault(objectSchema.description(), ""));
|
||||
if (properties.size() > 1) {
|
||||
builder.propertyOrdering(new ArrayList<>(properties.keySet()));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
if (element instanceof JsonStringSchema stringSchema) {
|
||||
|
|
|
|||
|
|
@ -425,6 +425,48 @@ class GoogleGenAiToolMapperTest {
|
|||
.hasMessageContaining("Unknown schema type");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_preserve_declared_property_order_and_set_property_ordering() {
|
||||
List<String> declaredOrder = List.of("gamma", "alpha", "zeta", "beta", "delta", "epsilon");
|
||||
JsonObjectSchema.Builder objectBuilder = JsonObjectSchema.builder();
|
||||
declaredOrder.forEach(name -> objectBuilder.addStringProperty(name));
|
||||
|
||||
ToolSpecification spec = ToolSpecification.builder()
|
||||
.name("test")
|
||||
.description("test")
|
||||
.parameters(objectBuilder.build())
|
||||
.build();
|
||||
|
||||
Schema schema =
|
||||
GoogleGenAiToolMapper.convertToGoogleFunction(spec).parameters().get();
|
||||
|
||||
assertThat(schema.properties().get().keySet()).containsExactlyElementsOf(declaredOrder);
|
||||
assertThat(schema.propertyOrdering().get()).containsExactlyElementsOf(declaredOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_set_property_ordering_on_nested_object() {
|
||||
List<String> nestedOrder = List.of("street", "number", "city", "zip");
|
||||
JsonObjectSchema.Builder addressBuilder = JsonObjectSchema.builder();
|
||||
nestedOrder.forEach(name -> addressBuilder.addStringProperty(name));
|
||||
|
||||
ToolSpecification spec = ToolSpecification.builder()
|
||||
.name("test")
|
||||
.description("test")
|
||||
.parameters(JsonObjectSchema.builder()
|
||||
.addProperty("address", addressBuilder.build())
|
||||
.addStringProperty("name")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Schema schema =
|
||||
GoogleGenAiToolMapper.convertToGoogleFunction(spec).parameters().get();
|
||||
Schema addressSchema = schema.properties().get().get("address");
|
||||
|
||||
assertThat(schema.propertyOrdering().get()).containsExactly("address", "name");
|
||||
assertThat(addressSchema.propertyOrdering().get()).containsExactlyElementsOf(nestedOrder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_convert_nested_object_schema() {
|
||||
ToolSpecification spec = ToolSpecification.builder()
|
||||
|
|
|
|||
Loading…
Reference in New Issue