Skip to content

Commit 9f550f2

Browse files
committed
Merge branch 'main' into reconcile-v0_10-superset
2 parents da669ed + 139c930 commit 9f550f2

5 files changed

Lines changed: 97 additions & 63 deletions

File tree

agent_sdks/kotlin/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ repositories {
3939

4040
dependencies {
4141
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
42-
implementation("com.networknt:json-schema-validator:1.5.1")
42+
implementation("com.networknt:json-schema-validator:2.0.1")
4343
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
4444

4545
// Core Dependencies

agent_sdks/kotlin/src/main/kotlin/com/google/a2ui/schema/Validator.kt

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
package com.google.a2ui.schema
1818

19-
import com.fasterxml.jackson.databind.ObjectMapper
20-
import com.networknt.schema.JsonSchema
21-
import com.networknt.schema.JsonSchemaFactory
22-
import com.networknt.schema.SchemaValidatorsConfig
23-
import com.networknt.schema.SpecVersion
19+
import com.networknt.schema.InputFormat
20+
import com.networknt.schema.Schema
21+
import com.networknt.schema.SchemaRegistry
22+
import com.networknt.schema.SchemaRegistryConfig
23+
import com.networknt.schema.dialect.Dialects
2424
import kotlinx.serialization.json.Json
2525
import kotlinx.serialization.json.JsonArray
2626
import kotlinx.serialization.json.JsonElement
@@ -44,21 +44,18 @@ constructor(
4444
private val catalog: A2uiCatalog,
4545
private val schemaMappings: Map<String, String> = emptyMap(),
4646
) {
47-
private val validator: JsonSchema = buildValidator()
48-
private val mapper = ObjectMapper()
49-
private val shared0_9Factory: JsonSchemaFactory by lazy {
50-
JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012))
51-
.schemaMappers { schemaMappers ->
52-
schemaMappings.forEach { (prefix, target) -> schemaMappers.mapPrefix(prefix, target) }
47+
private val shared0_9Registry: SchemaRegistry by lazy {
48+
SchemaRegistry.withDialect(Dialects.getDraft202012()) { builder ->
49+
builder.schemaIdResolvers { schemaIdResolvers ->
50+
schemaMappings.forEach { (prefix, target) -> schemaIdResolvers.mapPrefix(prefix, target) }
5351
}
54-
.build()
55-
}
56-
private val sharedConfig: SchemaValidatorsConfig by lazy {
57-
SchemaValidatorsConfig.builder().build()
52+
}
5853
}
59-
private val subValidators = mutableMapOf<String, JsonSchema>()
54+
private val sharedConfig: SchemaRegistryConfig by lazy { SchemaRegistryConfig.builder().build() }
55+
private val subValidators = mutableMapOf<String, Schema>()
56+
private val validator: Schema = buildValidator()
6057

61-
private fun buildValidator(): JsonSchema =
58+
private fun buildValidator(): Schema =
6259
if (catalog.version == A2uiVersion.VERSION_0_8) build0_8Validator() else build0_9Validator()
6360

6461
private fun injectAdditionalProperties(
@@ -121,7 +118,7 @@ constructor(
121118
return bundled as JsonObject
122119
}
123120

124-
private fun build0_8Validator(): JsonSchema {
121+
private fun build0_8Validator(): Schema {
125122
val bundledSchema = bundle0_8Schemas()
126123
val fullSchema = SchemaResourceLoader.wrapAsJsonArray(bundledSchema).toMutableMap()
127124
fullSchema[KEY_DOLLAR_SCHEMA] = JsonPrimitive(SCHEMA_DRAFT_2020_12)
@@ -132,38 +129,29 @@ constructor(
132129
val baseDirUri = baseUri.substringBeforeLast("/")
133130
val commonTypesUri = "$baseDirUri/$FILE_COMMON_TYPES"
134131

135-
val config = SchemaValidatorsConfig.builder().build()
136-
137132
val jsonFmt = Json { prettyPrint = false }
138133

139-
val factory =
140-
JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012))
141-
.schemaMappers { schemaMappers ->
142-
schemaMappings.forEach { (prefix, target) -> schemaMappers.mapPrefix(prefix, target) }
143-
schemaMappers.mapPrefix(FILE_COMMON_TYPES, commonTypesUri)
134+
val registry =
135+
SchemaRegistry.withDialect(Dialects.getDraft202012()) { builder ->
136+
builder.schemaIdResolvers { schemaIdResolvers ->
137+
schemaMappings.forEach { (prefix, target) -> schemaIdResolvers.mapPrefix(prefix, target) }
138+
schemaIdResolvers.mapPrefix(FILE_COMMON_TYPES, commonTypesUri)
144139
}
145-
.build()
140+
builder.schemaRegistryConfig(sharedConfig)
141+
}
146142

147143
val schemaString = jsonFmt.encodeToString(JsonElement.serializer(), JsonObject(fullSchema))
148-
return factory.getSchema(schemaString, config)
144+
return registry.getSchema(schemaString, InputFormat.JSON)
149145
}
150146

151-
private fun build0_9Validator(): JsonSchema {
147+
private fun build0_9Validator(): Schema {
152148
val fullSchema =
153149
SchemaResourceLoader.wrapAsJsonArray(catalog.serverToClientSchema).toMutableMap()
154150
fullSchema[KEY_DOLLAR_SCHEMA] = JsonPrimitive(SCHEMA_DRAFT_2020_12)
155151

156-
val config = SchemaValidatorsConfig.builder().build()
157-
val factory =
158-
JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012))
159-
.schemaMappers { schemaMappers ->
160-
schemaMappings.forEach { (prefix, target) -> schemaMappers.mapPrefix(prefix, target) }
161-
}
162-
.build()
163-
164152
val jsonFmt = Json { prettyPrint = false }
165153
val schemaString = jsonFmt.encodeToString(JsonElement.serializer(), JsonObject(fullSchema))
166-
return factory.getSchema(schemaString, config)
154+
return shared0_9Registry.getSchema(schemaString, InputFormat.JSON)
167155
}
168156

169157
/**
@@ -185,9 +173,8 @@ constructor(
185173
// Basic schema validation
186174
val jsonFmt = Json { prettyPrint = false }
187175
val messagesString = jsonFmt.encodeToString(JsonElement.serializer(), messages)
188-
val jsonNode = mapper.readTree(messagesString)
189176

190-
val errors = validator.validate(jsonNode)
177+
val errors = validator.validate(messagesString, InputFormat.JSON)
191178
if (errors.isNotEmpty()) {
192179
val msg = buildString {
193180
append("Validation failed:")
@@ -290,7 +277,7 @@ constructor(
290277
}
291278
}
292279

293-
private fun getSubValidator(defName: String): JsonSchema {
280+
private fun getSubValidator(defName: String): Schema {
294281
return subValidators.getOrPut(defName) {
295282
val defs =
296283
catalog.serverToClientSchema["\$defs"] as? JsonObject
@@ -310,22 +297,21 @@ constructor(
310297

311298
val jsonFmt = Json { prettyPrint = false }
312299
val schemaString = jsonFmt.encodeToString(JsonElement.serializer(), tempSchema)
313-
shared0_9Factory.getSchema(schemaString, sharedConfig)
300+
shared0_9Registry.getSchema(schemaString, InputFormat.JSON)
314301
}
315302
}
316303

317304
private fun getFormattedErrors(
318-
validator: JsonSchema,
305+
validator: Schema,
319306
instance: JsonElement,
320307
basePath: String,
321308
): List<String> {
322309
val jsonFmt = Json { prettyPrint = false }
323310
val instanceStr = jsonFmt.encodeToString(JsonElement.serializer(), instance)
324-
val jsonNode = mapper.readTree(instanceStr)
325-
val errors = validator.validate(jsonNode)
311+
val errors = validator.validate(instanceStr, InputFormat.JSON)
326312

327313
return errors.map { err ->
328-
val msg = err.message ?: ""
314+
val msg = err.toString()
329315
val unexpectedRegex =
330316
Regex(
331317
"property '(.*?)' is not defined in the schema and the schema does not allow additional properties"
@@ -419,7 +405,7 @@ constructor(
419405
)
420406
val jsonFmt = Json { prettyPrint = false }
421407
val schemaString = jsonFmt.encodeToString(JsonElement.serializer(), tempSchema)
422-
shared0_9Factory.getSchema(schemaString, sharedConfig)
408+
shared0_9Registry.getSchema(schemaString, InputFormat.JSON)
423409
}
424410

425411
return getFormattedErrors(validator, comp, path)

agent_sdks/python/src/a2ui/adk/a2a/part_converter.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,25 @@ class A2uiPartConverter:
5555
This converter handles both tool-based A2UI (via `send_a2ui_json_to_client`)
5656
and text-based A2UI (via A2UI delimiter tags). It uses the provided
5757
catalog to validate and fix JSON payloads.
58+
59+
Args:
60+
a2ui_catalog: The A2UI catalog.
61+
bypass_tool_check: If True, bypass tool validation.
62+
fallback_text: Optional text to fall back on if parsing fails.
63+
version: The A2UI version to use. Defaults to "0.8".
5864
"""
5965

6066
def __init__(
6167
self,
6268
a2ui_catalog: A2uiCatalog,
6369
bypass_tool_check: bool = False,
6470
fallback_text: Optional[str] = None,
71+
version: str = constants.VERSION_0_8,
6572
):
6673
self._catalog = a2ui_catalog
6774
self._bypass_tool_check = bypass_tool_check
6875
self._fallback_text = fallback_text
76+
self._version = version
6977

7078
def convert(self, part: genai_types.Part) -> list[a2a_types.Part]:
7179
"""Converts a GenAI part to A2A parts, with A2UI validation.
@@ -97,7 +105,10 @@ def convert(self, part: genai_types.Part) -> list[a2a_types.Part]:
97105
):
98106
json_data = response_dict.get(constants.A2UI_VALIDATED_JSON_KEY)
99107
if json_data:
100-
return [create_a2ui_part(message) for message in json_data]
108+
return [
109+
create_a2ui_part(message, version=self._version)
110+
for message in json_data
111+
]
101112

102113
if is_send_a2ui_json_to_client_response:
103114
logger.info("No result in A2UI tool response")
@@ -111,6 +122,7 @@ def convert(self, part: genai_types.Part) -> list[a2a_types.Part]:
111122
result,
112123
validator=self._catalog.validator,
113124
fallback_text=self._fallback_text,
125+
version=self._version,
114126
)
115127

116128
# 2. Handle Tool Calls (FunctionCall) - Skip sending to client
@@ -126,6 +138,7 @@ def convert(self, part: genai_types.Part) -> list[a2a_types.Part]:
126138
text,
127139
validator=self._catalog.validator,
128140
fallback_text=self._fallback_text,
141+
version=self._version,
129142
)
130143

131144
# 4. Default conversion for other parts

agent_sdks/python/tests/adk/a2a/test_part_converter.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from a2ui.adk.a2a.part_converter import A2uiPartConverter
2525
from a2ui.adk.send_a2ui_to_client_toolset import SendA2uiToClientToolset
2626
from a2ui.schema.catalog import A2uiCatalog
27-
from a2ui.schema.constants import A2UI_CLOSE_TAG, A2UI_OPEN_TAG
27+
from a2ui.schema.constants import A2UI_CLOSE_TAG, A2UI_OPEN_TAG, VERSION_0_8, VERSION_0_9_1
2828
from google.genai import types as genai_types
2929

3030

@@ -45,7 +45,27 @@ def test_converter_class_convert_valid_tool_response():
4545

4646
a2a_parts = converter.convert(part)
4747
assert len(a2a_parts) == 1
48-
assert a2a_parts[0] == create_a2ui_part(valid_a2ui)
48+
assert a2a_parts[0] == create_a2ui_part(valid_a2ui, version=VERSION_0_8)
49+
50+
51+
def test_converter_class_convert_valid_tool_response_v0_9_1():
52+
catalog_mock = MagicMock(spec=A2uiCatalog)
53+
converter = A2uiPartConverter(catalog_mock, version=VERSION_0_9_1)
54+
55+
valid_a2ui = {"type": "Text", "text": "Hello"}
56+
function_response = genai_types.FunctionResponse(
57+
name=SendA2uiToClientToolset._SendA2uiJsonToClientTool.TOOL_NAME,
58+
response={
59+
SendA2uiToClientToolset._SendA2uiJsonToClientTool.VALIDATED_A2UI_JSON_KEY: [
60+
valid_a2ui
61+
]
62+
},
63+
)
64+
part = genai_types.Part(function_response=function_response)
65+
66+
a2a_parts = converter.convert(part)
67+
assert len(a2a_parts) == 1
68+
assert a2a_parts[0] == create_a2ui_part(valid_a2ui, version=VERSION_0_9_1)
4969

5070

5171
def test_converter_class_convert_tool_error_response():
@@ -107,7 +127,26 @@ def test_converter_class_convert_text_with_a2ui():
107127
# Expect 2 parts: TextPart and A2UI DataPart
108128
assert len(a2a_parts) == 2
109129
assert a2a_parts[0].root.text == "Here is the UI:"
110-
assert a2a_parts[1] == create_a2ui_part(valid_a2ui[0])
130+
assert a2a_parts[1] == create_a2ui_part(valid_a2ui[0], version=VERSION_0_8)
131+
catalog_mock.validator.validate.assert_called_once_with(valid_a2ui)
132+
133+
134+
def test_converter_class_convert_text_with_a2ui_v0_9_1():
135+
catalog_mock = MagicMock(spec=A2uiCatalog)
136+
converter = A2uiPartConverter(catalog_mock, version=VERSION_0_9_1)
137+
138+
valid_a2ui = [{"type": "Text", "text": "Hello"}]
139+
catalog_mock.validator.validate.return_value = None
140+
141+
text = f"Here is the UI:\n{A2UI_OPEN_TAG}\n{json.dumps(valid_a2ui)}\n{A2UI_CLOSE_TAG}"
142+
part = genai_types.Part(text=text)
143+
144+
a2a_parts = converter.convert(part)
145+
146+
# Expect 2 parts: TextPart and A2UI DataPart
147+
assert len(a2a_parts) == 2
148+
assert a2a_parts[0].root.text == "Here is the UI:"
149+
assert a2a_parts[1] == create_a2ui_part(valid_a2ui[0], version=VERSION_0_9_1)
111150
catalog_mock.validator.validate.assert_called_once_with(valid_a2ui)
112151

113152

@@ -123,7 +162,7 @@ def test_converter_class_convert_text_empty_leading():
123162
a2a_parts = converter.convert(part)
124163

125164
assert len(a2a_parts) == 1
126-
assert a2a_parts[0] == create_a2ui_part(ui[0])
165+
assert a2a_parts[0] == create_a2ui_part(ui[0], version=VERSION_0_8)
127166

128167

129168
def test_converter_class_convert_text_markdown_wrapped():
@@ -140,7 +179,7 @@ def test_converter_class_convert_text_markdown_wrapped():
140179

141180
assert len(a2a_parts) == 2
142181
assert a2a_parts[0].root.text == "Behold:"
143-
assert a2a_parts[1] == create_a2ui_part(ui[0])
182+
assert a2a_parts[1] == create_a2ui_part(ui[0], version=VERSION_0_8)
144183
catalog_mock.validator.validate.assert_called_once_with(ui)
145184

146185

@@ -197,7 +236,7 @@ def test_converter_class_convert_tool_response_with_result_containing_a2ui():
197236
# Expect 2 parts: TextPart and A2UI DataPart
198237
assert len(a2a_parts) == 2
199238
assert a2a_parts[0].root.text == "Here is the result:"
200-
assert a2a_parts[1] == create_a2ui_part(valid_a2ui[0])
239+
assert a2a_parts[1] == create_a2ui_part(valid_a2ui[0], version=VERSION_0_8)
201240
catalog_mock.validator.validate.assert_called_once_with(valid_a2ui)
202241

203242

samples/client/flutter/restaurant_finder/e2e_test/test/infra_test.dart

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,9 @@ void main() {
3030
print('Joke from AI:\n\n$result\n\n');
3131
});
3232

33-
test(
34-
'test can start restaurant_finder',
35-
() async {
36-
final restaurantFinderClient = TestRestaurantFinderClient();
37-
addTearDown(restaurantFinderClient.dispose);
38-
await restaurantFinderClient.startAndVerify();
39-
},
40-
timeout: const Timeout(Duration(minutes: 5)),
41-
);
33+
test('test can start restaurant_finder', () async {
34+
final restaurantFinderClient = TestRestaurantFinderClient();
35+
addTearDown(restaurantFinderClient.dispose);
36+
await restaurantFinderClient.startAndVerify();
37+
}, timeout: const Timeout(Duration(minutes: 5)));
4238
}

0 commit comments

Comments
 (0)