Skip to content

Commit 405f60f

Browse files
committed
Merge branch 'main' of github.com:a2ui-project/a2ui into python-release
2 parents 0e382cb + 160df9c commit 405f60f

58 files changed

Lines changed: 230 additions & 201 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ If you need help, consider asking for advice on the [discussion board].
2121
[CHANGELOG]: ../CHANGELOG.md
2222
[CLA]: https://cla.developers.google.com/
2323
[Contributors Guide]: ../CONTRIBUTING.md
24-
[discussion board]: https://github.com/google/A2UI/discussions
24+
[discussion board]: https://github.com/a2ui-project/a2ui/discussions
2525
[Style Guide]: ../CONTRIBUTING.md#coding-style

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
contents: write
4040
actions: read
4141

42-
if: github.repository == 'google/A2UI'
42+
if: github.repository == 'a2ui-project/a2ui'
4343

4444
steps:
4545
- name: Checkout Code

.github/workflows/e2e_test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
e2e_test:
1616
# Do not run on forked branches,
1717
# because the test does not have access to secrets in forks.
18-
if: github.repository == 'google/a2ui'
18+
if: github.repository == 'a2ui-project/a2ui'
1919
runs-on: ubuntu-latest
2020
permissions:
2121
contents: read

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ Pick the path that matches where you want to start:
114114
Prerequisites: Node.js 18+, [uv](https://docs.astral.sh/uv/), and a [Gemini API key](https://aistudio.google.com/apikey).
115115

116116
```bash
117-
git clone https://github.com/google/A2UI.git
118-
cd A2UI
117+
git clone https://github.com/a2ui-project/a2ui.git
118+
cd a2ui
119119
export GEMINI_API_KEY="your_gemini_api_key"
120120
cd samples/client/lit
121121
npm run demo:restaurant

agent_sdks/agent_sdk_guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ Create the helper utilities to wrap JSON in transport Parts (if needed for your
262262

263263
### Step 5: Sample Applications
264264

265-
Create a simple sample (like a command-line agent or local server) to verify that the SDK works end-to-end. Refer to the reference Python samples (e.g., `samples/agent/adk/contact_lookup`) for inspiration.
265+
Create a simple sample (like a command-line agent or local server) to verify that the SDK works end-to-end. Refer to the reference Python samples (e.g., `samples/agent/adk/restaurant_finder`) for inspiration.
266266

267267
> [!IMPORTANT]
268268
> Keep the SDK idiomatic to your language. Don't force Python-isms if it doesn't make sense (e.g., use builder patterns in Java/Kotlin or macros in C++ if they are more ergonomic).

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

docs/ecosystem/a2ui-in-the-world.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ ADK integrated the A2UI v0.8 basic catalog to automatically render spec-complian
8080

8181
- **Built-in rendering**: ADK Web renders A2UI components natively in the dev UI.
8282
- **A2A integration**: A2UI messages are converted between A2A DataPart metadata and ADK events.
83-
- **Agent SDK**: The [A2UI Python agent SDK](https://github.com/google/A2UI/tree/main/agent_sdks/python) provides an ADK extension for generating A2UI from agents.
83+
- **Agent SDK**: The [A2UI Python agent SDK](https://github.com/a2ui-project/a2ui/tree/main/agent_sdks/python) provides an ADK extension for generating A2UI from agents.
8484

8585
**Try it:**
8686

@@ -165,17 +165,12 @@ The A2UI community is building exciting projects:
165165

166166
### Open Source Examples
167167

168-
- **Restaurant Finder** ([samples/agent/adk/restaurant_finder](https://github.com/google/A2UI/tree/main/samples/agent/adk/restaurant_finder))
168+
- **Restaurant Finder** ([samples/agent/adk/restaurant_finder](https://github.com/a2ui-project/a2ui/tree/main/samples/agent/adk/restaurant_finder))
169169
- Table reservation with dynamic forms
170170
- Gemini-powered agent
171171
- Full source code available
172172

173-
- **Contact Lookup** ([samples/agent/adk/contact_lookup](https://github.com/google/A2UI/tree/main/samples/agent/adk/contact_lookup))
174-
- Search interface with results list
175-
- A2A agent example
176-
- Demonstrates data binding
177-
178-
- **Component Gallery** ([samples/client/angular - gallery mode](https://github.com/google/A2UI/tree/main/samples/client/angular))
173+
- **Component Gallery** ([samples/client/angular - gallery mode](https://github.com/a2ui-project/a2ui/tree/main/samples/client/angular))
179174
- Interactive showcase of all components
180175
- Live examples with code
181176
- Great for learning
@@ -202,4 +197,4 @@ For more information, see the following resources:
202197

203198
---
204199

205-
**Using A2UI in production?** Share your story on [GitHub Discussions](https://github.com/google/A2UI/discussions).
200+
**Using A2UI in production?** Share your story on [GitHub Discussions](https://github.com/a2ui-project/a2ui/discussions).

0 commit comments

Comments
 (0)