Skip to content

Commit 6ce8b1a

Browse files
committed
added a2ui support and display part
1 parent 60ebe8f commit 6ce8b1a

File tree

4 files changed

+354
-1
lines changed

4 files changed

+354
-1
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>io.github.vishalmysore</groupId>
88
<artifactId>a2ajava</artifactId>
9-
<version>1.0.0</version>
9+
<version>1.0.1</version>
1010
<name>A2A Protocol Implementation for Java</name>
1111
<description>
1212
Java implementation of the A2A protocol v1.0, which allows for the exchange of data between different AI systems.

src/main/java/io/github/vishalmysore/a2a/domain/DataPart.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,27 @@ public static DataPart createA2UIPart(Map<String, Object> a2uiMessage) {
7070
part.setMetadata(metadata);
7171
return part;
7272
}
73+
74+
@JsonIgnore
75+
public String dataToText() {
76+
StringBuilder sb = new StringBuilder();
77+
78+
sb.append("Type: ").append(type).append("\n");
79+
80+
if (metadata != null && !metadata.isEmpty()) {
81+
sb.append("Metadata:\n");
82+
metadata.forEach((key, value) ->
83+
sb.append(" ").append(key).append(": ").append(value).append("\n")
84+
);
85+
}
86+
87+
if (data != null && !data.isEmpty()) {
88+
sb.append("Data:\n");
89+
data.forEach((key, value) ->
90+
sb.append(" ").append(key).append(": ").append(value).append("\n")
91+
);
92+
}
93+
94+
return sb.toString().trim();
95+
}
7396
}

src/main/java/io/github/vishalmysore/a2a/server/DyanamicTaskContoller.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,16 @@ private void processParts(TaskSendParams taskSendParams, Task task, String taskI
152152
processTextPart(textPart, task, actionCallback);
153153
} else if (part instanceof FilePart) {
154154
processFileTaskLogic(taskSendParams, task, taskId, actionCallback);
155+
} else if (part instanceof DataPart) {
156+
// Handle DataPart if needed
157+
158+
String text = ((DataPart)part).dataToText();
159+
if (actionCallback != null) {
160+
processWithCallback(text, task, actionCallback);
161+
} else {
162+
processWithoutCallback(text, task);
163+
}
164+
log.info("Received DataPart, processing as text.");
155165
}
156166
}
157167

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
package io.github.vishalmysore.a2ui;
2+
3+
import com.t4a.detect.ActionCallback;
4+
import io.github.vishalmysore.common.CallBackType;
5+
6+
import java.util.*;
7+
8+
/**
9+
* Utility interface for creating A2UI display components
10+
* Provides reusable methods for building A2UI surface updates, components, and rendering messages
11+
*/
12+
public interface A2UIDisplay {
13+
14+
/**
15+
* Creates a surface update map with the given surface ID
16+
* @param surfaceId The ID of the surface
17+
* @return Map containing surfaceId
18+
*/
19+
default Map<String, Object> createSurfaceUpdate(String surfaceId) {
20+
Map<String, Object> surfaceUpdate = new HashMap<>();
21+
surfaceUpdate.put("surfaceId", surfaceId);
22+
return surfaceUpdate;
23+
}
24+
25+
/**
26+
* Creates a root column component with child IDs
27+
* @param rootId The ID of the root component
28+
* @param childIds List of child component IDs
29+
* @return Map representing the root column component
30+
*/
31+
default Map<String, Object> createRootColumn(String rootId, List<String> childIds) {
32+
Map<String, Object> root = new HashMap<>();
33+
root.put("id", rootId);
34+
35+
Map<String, Object> rootComponent = new HashMap<>();
36+
Map<String, Object> columnProps = new HashMap<>();
37+
columnProps.put("children", new HashMap<String, Object>() {{
38+
put("explicitList", childIds);
39+
}});
40+
rootComponent.put("Column", columnProps);
41+
root.put("component", rootComponent);
42+
43+
return root;
44+
}
45+
46+
/**
47+
* Creates a text component
48+
* @param id The component ID
49+
* @param text The text content
50+
* @return Map representing the text component
51+
*/
52+
default Map<String, Object> createTextComponent(String id, String text) {
53+
return createTextComponent(id, text, null);
54+
}
55+
56+
/**
57+
* Creates a text component with usage hint
58+
* @param id The component ID
59+
* @param text The text content
60+
* @param usageHint Optional usage hint (e.g., "h1", "h2", "body")
61+
* @return Map representing the text component
62+
*/
63+
default Map<String, Object> createTextComponent(String id, String text, String usageHint) {
64+
Map<String, Object> component = new HashMap<>();
65+
component.put("id", id);
66+
67+
Map<String, Object> textComponent = new HashMap<>();
68+
Map<String, Object> textProps = new HashMap<>();
69+
textProps.put("text", new HashMap<String, Object>() {{
70+
put("literalString", text);
71+
}});
72+
73+
if (usageHint != null && !usageHint.isEmpty()) {
74+
textProps.put("usageHint", usageHint);
75+
}
76+
77+
textComponent.put("Text", textProps);
78+
component.put("component", textComponent);
79+
80+
return component;
81+
}
82+
83+
/**
84+
* Creates a data model update
85+
* @param surfaceId The surface ID
86+
* @return Map representing the data model update
87+
*/
88+
default Map<String, Object> createDataModelUpdate(String surfaceId) {
89+
Map<String, Object> dataModelUpdate = new HashMap<>();
90+
dataModelUpdate.put("surfaceId", surfaceId);
91+
dataModelUpdate.put("contents", new ArrayList<>());
92+
return dataModelUpdate;
93+
}
94+
95+
/**
96+
* Creates a data model update with initial values using adjacency list format (A2UI v0.8 spec)
97+
* @param surfaceId The surface ID
98+
* @param initialValues Map of data paths to initial values (e.g., "/form/name" -> "")
99+
* @return Map representing the data model update
100+
*/
101+
default Map<String, Object> createDataModelUpdateWithValues(String surfaceId, Map<String, Object> initialValues) {
102+
Map<String, Object> dataModelUpdate = new HashMap<>();
103+
dataModelUpdate.put("surfaceId", surfaceId);
104+
105+
List<Map<String, Object>> contents = new ArrayList<>();
106+
if (initialValues != null && !initialValues.isEmpty()) {
107+
// Group paths by their root key (e.g., "/form/name" -> root key is "form")
108+
Map<String, List<Map.Entry<String, Object>>> groupedByRoot = new LinkedHashMap<>();
109+
110+
for (Map.Entry<String, Object> entry : initialValues.entrySet()) {
111+
String path = entry.getKey();
112+
// Parse path: "/form/name" -> root="form", subKey="name"
113+
String[] parts = path.split("/");
114+
if (parts.length >= 2) {
115+
String rootKey = parts[1]; // Skip empty string from leading /
116+
groupedByRoot.computeIfAbsent(rootKey, k -> new ArrayList<>()).add(entry);
117+
}
118+
}
119+
120+
// Build adjacency list format
121+
for (Map.Entry<String, List<Map.Entry<String, Object>>> rootEntry : groupedByRoot.entrySet()) {
122+
Map<String, Object> rootItem = new HashMap<>();
123+
rootItem.put("key", rootEntry.getKey());
124+
125+
List<Map<String, Object>> valueMap = new ArrayList<>();
126+
for (Map.Entry<String, Object> valueEntry : rootEntry.getValue()) {
127+
String fullPath = valueEntry.getKey();
128+
String[] pathParts = fullPath.split("/");
129+
130+
// Handle nested paths like "/reservation/menu/appetizer"
131+
if (pathParts.length == 3) {
132+
// Simple path: /form/name -> key="name"
133+
Map<String, Object> valueItem = new HashMap<>();
134+
valueItem.put("key", pathParts[2]);
135+
valueItem.put("valueString", valueEntry.getValue());
136+
valueMap.add(valueItem);
137+
} else if (pathParts.length > 3) {
138+
// Nested path: /reservation/menu/appetizer
139+
// Create nested structure
140+
String subKey = pathParts[2];
141+
String leafKey = String.join("/", Arrays.copyOfRange(pathParts, 3, pathParts.length));
142+
143+
Map<String, Object> valueItem = new HashMap<>();
144+
valueItem.put("key", subKey + "/" + leafKey);
145+
valueItem.put("valueString", valueEntry.getValue());
146+
valueMap.add(valueItem);
147+
}
148+
}
149+
150+
rootItem.put("valueMap", valueMap);
151+
contents.add(rootItem);
152+
}
153+
}
154+
dataModelUpdate.put("contents", contents);
155+
return dataModelUpdate;
156+
}
157+
158+
/**
159+
* Creates a begin rendering message
160+
* @param surfaceId The surface ID
161+
* @param rootId The root component ID
162+
* @return Map representing the begin rendering message
163+
*/
164+
default Map<String, Object> createBeginRendering(String surfaceId, String rootId) {
165+
Map<String, Object> beginRendering = new HashMap<>();
166+
beginRendering.put("surfaceId", surfaceId);
167+
beginRendering.put("root", rootId);
168+
return beginRendering;
169+
}
170+
171+
/**
172+
* Creates a TextField component for user input with data model binding (A2UI v0.8 spec compliant)
173+
* @param id The component ID
174+
* @param label The label for the input field
175+
* @param dataPath The path in the data model (e.g., "/form/name")
176+
* @return Map representing the TextField component
177+
*/
178+
default Map<String, Object> createTextFieldComponent(String id, String label, String dataPath) {
179+
Map<String, Object> component = new HashMap<>();
180+
component.put("id", id);
181+
182+
Map<String, Object> textFieldComponent = new HashMap<>();
183+
Map<String, Object> textFieldProps = new HashMap<>();
184+
185+
// Label is required for TextField
186+
textFieldProps.put("label", new HashMap<String, Object>() {{
187+
put("literalString", label);
188+
}});
189+
190+
// Bind text to data model path (A2UI v0.8 uses 'text' property for TextField)
191+
textFieldProps.put("text", new HashMap<String, Object>() {{
192+
put("path", dataPath);
193+
}});
194+
195+
textFieldComponent.put("TextField", textFieldProps);
196+
component.put("component", textFieldComponent);
197+
198+
return component;
199+
}
200+
201+
/**
202+
* Creates a Button component with action and context (A2UI v0.8 spec compliant)
203+
* @param id The component ID
204+
* @param buttonText The text to display on the button
205+
* @param actionName The action name to trigger
206+
* @param contextBindings Map of parameter names to data paths (e.g., "personName" -> "/form/name")
207+
* @return Map representing the Button component
208+
*/
209+
default Map<String, Object> createButtonComponent(String id, String buttonText, String actionName, Map<String, String> contextBindings) {
210+
Map<String, Object> component = new HashMap<>();
211+
component.put("id", id);
212+
213+
Map<String, Object> buttonComponent = new HashMap<>();
214+
Map<String, Object> buttonProps = new HashMap<>();
215+
216+
// Button requires a child component (typically Text) and an action
217+
String childTextId = id + "_text";
218+
buttonProps.put("child", childTextId);
219+
220+
// Action with context for data binding
221+
Map<String, Object> action = new HashMap<>();
222+
action.put("name", actionName);
223+
224+
// Add context array if bindings are provided
225+
if (contextBindings != null && !contextBindings.isEmpty()) {
226+
List<Map<String, Object>> context = new ArrayList<>();
227+
for (Map.Entry<String, String> binding : contextBindings.entrySet()) {
228+
Map<String, Object> contextItem = new HashMap<>();
229+
contextItem.put("key", binding.getKey());
230+
contextItem.put("value", new HashMap<String, Object>() {{
231+
put("path", binding.getValue());
232+
}});
233+
context.add(contextItem);
234+
}
235+
action.put("context", context);
236+
}
237+
238+
buttonProps.put("action", action);
239+
240+
buttonComponent.put("Button", buttonProps);
241+
component.put("component", buttonComponent);
242+
243+
return component;
244+
}
245+
246+
/**
247+
* Creates a Button component with action (no context) - for simple buttons
248+
* @param id The component ID
249+
* @param buttonText The text to display on the button
250+
* @param actionName The action name to trigger
251+
* @return Map representing the Button component
252+
*/
253+
default Map<String, Object> createButtonComponent(String id, String buttonText, String actionName) {
254+
return createButtonComponent(id, buttonText, actionName, null);
255+
}
256+
257+
/**
258+
* Creates a Text component for button child
259+
* @param id The component ID
260+
* @param text The text content
261+
* @return Map representing the Text component
262+
*/
263+
default Map<String, Object> createButtonTextChild(String id, String text) {
264+
return createTextComponent(id, text);
265+
}
266+
267+
/**
268+
* Builds a complete A2UI message with surface update, data model update, and begin rendering
269+
* @param surfaceId The surface ID
270+
* @param rootId The root component ID
271+
* @param components List of all components including root
272+
* @return Complete A2UI message map
273+
*/
274+
default Map<String, Object> buildA2UIMessage(String surfaceId, String rootId, List<Map<String, Object>> components) {
275+
Map<String, Object> messages = new LinkedHashMap<>();
276+
277+
// 1. Surface update with components
278+
Map<String, Object> surfaceUpdate = createSurfaceUpdate(surfaceId);
279+
surfaceUpdate.put("components", components);
280+
messages.put("surfaceUpdate", surfaceUpdate);
281+
282+
// 2. Data model update
283+
messages.put("dataModelUpdate", createDataModelUpdate(surfaceId));
284+
285+
// 3. Begin rendering
286+
messages.put("beginRendering", createBeginRendering(surfaceId, rootId));
287+
288+
return messages;
289+
}
290+
291+
/**
292+
* Builds a complete A2UI message with surface update, data model update with initial values, and begin rendering
293+
* @param surfaceId The surface ID
294+
* @param rootId The root component ID
295+
* @param components List of all components including root
296+
* @param dataModelValues Map of data paths to initial values
297+
* @return Complete A2UI message map
298+
*/
299+
default Map<String, Object> buildA2UIMessageWithData(String surfaceId, String rootId,
300+
List<Map<String, Object>> components,
301+
Map<String, Object> dataModelValues) {
302+
Map<String, Object> messages = new LinkedHashMap<>();
303+
304+
// 1. Surface update with components
305+
Map<String, Object> surfaceUpdate = createSurfaceUpdate(surfaceId);
306+
surfaceUpdate.put("components", components);
307+
messages.put("surfaceUpdate", surfaceUpdate);
308+
309+
// 2. Data model update with initial values
310+
messages.put("dataModelUpdate", createDataModelUpdateWithValues(surfaceId, dataModelValues));
311+
312+
// 3. Begin rendering
313+
messages.put("beginRendering", createBeginRendering(surfaceId, rootId));
314+
315+
return messages;
316+
}
317+
default boolean isUICallback(ThreadLocal<ActionCallback> callback) {
318+
return callback != null && callback.get()!=null && callback.get().getType().equals(CallBackType.A2UI.name());
319+
}
320+
}

0 commit comments

Comments
 (0)