Skip to content

Commit f9143c2

Browse files
google-genai-botcopybara-github
authored andcommitted
refactor: Extracted the session json <-> Object conversion mapping from VertexAiSessionService
I added auto generated tests, which found a problem in artifactDelta conversion PiperOrigin-RevId: 803600227
1 parent 4f245f6 commit f9143c2

File tree

3 files changed

+504
-217
lines changed

3 files changed

+504
-217
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.adk.sessions;
18+
19+
import com.fasterxml.jackson.core.JsonProcessingException;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.google.adk.JsonBaseModel;
22+
import com.google.adk.events.Event;
23+
import com.google.adk.events.EventActions;
24+
import com.google.common.base.Splitter;
25+
import com.google.common.collect.ImmutableMap;
26+
import com.google.common.collect.Iterables;
27+
import com.google.genai.types.Content;
28+
import com.google.genai.types.FinishReason;
29+
import com.google.genai.types.GroundingMetadata;
30+
import com.google.genai.types.Part;
31+
import java.io.UncheckedIOException;
32+
import java.time.Instant;
33+
import java.util.HashMap;
34+
import java.util.HashSet;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.Optional;
38+
import java.util.concurrent.ConcurrentHashMap;
39+
import java.util.concurrent.ConcurrentMap;
40+
import javax.annotation.Nullable;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
43+
44+
/** Handles JSON serialization and deserialization for session-related objects. */
45+
final class SessionJsonConverter {
46+
private static final ObjectMapper objectMapper = JsonBaseModel.getMapper();
47+
private static final Logger logger = LoggerFactory.getLogger(SessionJsonConverter.class);
48+
49+
private SessionJsonConverter() {}
50+
51+
/**
52+
* Converts an {@link Event} to its JSON string representation for API transmission.
53+
*
54+
* @return JSON string of the event.
55+
* @throws UncheckedIOException if serialization fails.
56+
*/
57+
static String convertEventToJson(Event event) {
58+
Map<String, Object> metadataJson = new HashMap<>();
59+
metadataJson.put("partial", event.partial());
60+
metadataJson.put("turnComplete", event.turnComplete());
61+
metadataJson.put("interrupted", event.interrupted());
62+
metadataJson.put("branch", event.branch().orElse(null));
63+
metadataJson.put(
64+
"long_running_tool_ids",
65+
event.longRunningToolIds() != null ? event.longRunningToolIds().orElse(null) : null);
66+
if (event.groundingMetadata() != null) {
67+
metadataJson.put("grounding_metadata", event.groundingMetadata());
68+
}
69+
70+
Map<String, Object> eventJson = new HashMap<>();
71+
eventJson.put("author", event.author());
72+
eventJson.put("invocationId", event.invocationId());
73+
eventJson.put(
74+
"timestamp",
75+
new HashMap<>(
76+
ImmutableMap.of(
77+
"seconds",
78+
event.timestamp() / 1000,
79+
"nanos",
80+
(event.timestamp() % 1000) * 1000000)));
81+
if (event.errorCode().isPresent()) {
82+
eventJson.put("errorCode", event.errorCode());
83+
}
84+
if (event.errorMessage().isPresent()) {
85+
eventJson.put("errorMessage", event.errorMessage());
86+
}
87+
eventJson.put("eventMetadata", metadataJson);
88+
89+
if (event.actions() != null) {
90+
Map<String, Object> actionsJson = new HashMap<>();
91+
actionsJson.put("skipSummarization", event.actions().skipSummarization());
92+
actionsJson.put("stateDelta", event.actions().stateDelta());
93+
actionsJson.put("artifactDelta", event.actions().artifactDelta());
94+
actionsJson.put("transferAgent", event.actions().transferToAgent());
95+
actionsJson.put("escalate", event.actions().escalate());
96+
actionsJson.put("requestedAuthConfigs", event.actions().requestedAuthConfigs());
97+
eventJson.put("actions", actionsJson);
98+
}
99+
if (event.content().isPresent()) {
100+
eventJson.put("content", SessionUtils.encodeContent(event.content().get()));
101+
}
102+
if (event.errorCode().isPresent()) {
103+
eventJson.put("errorCode", event.errorCode().get());
104+
}
105+
if (event.errorMessage().isPresent()) {
106+
eventJson.put("errorMessage", event.errorMessage().get());
107+
}
108+
try {
109+
return objectMapper.writeValueAsString(eventJson);
110+
} catch (JsonProcessingException e) {
111+
throw new UncheckedIOException(e);
112+
}
113+
}
114+
115+
/**
116+
* Converts a raw value to a {@link Content} object.
117+
*
118+
* @return parsed {@link Content}, or {@code null} if conversion fails.
119+
*/
120+
@Nullable
121+
@SuppressWarnings("unchecked")
122+
private static Content convertMapToContent(Object rawContentValue) {
123+
if (rawContentValue == null) {
124+
return null;
125+
}
126+
127+
if (rawContentValue instanceof Map) {
128+
Map<String, Object> contentMap = (Map<String, Object>) rawContentValue;
129+
try {
130+
return objectMapper.convertValue(contentMap, Content.class);
131+
} catch (IllegalArgumentException e) {
132+
logger.warn("Error converting Map to Content", e);
133+
return null;
134+
}
135+
} else {
136+
logger.warn(
137+
"Unexpected type for 'content' in apiEvent: {}", rawContentValue.getClass().getName());
138+
return null;
139+
}
140+
}
141+
142+
/**
143+
* Converts raw API event data into an {@link Event} object.
144+
*
145+
* @return parsed {@link Event}.
146+
*/
147+
@SuppressWarnings("unchecked")
148+
static Event fromApiEvent(Map<String, Object> apiEvent) {
149+
EventActions eventActions = new EventActions();
150+
if (apiEvent.get("actions") != null) {
151+
Map<String, Object> actionsMap = (Map<String, Object>) apiEvent.get("actions");
152+
eventActions.setSkipSummarization(
153+
Optional.ofNullable(actionsMap.get("skipSummarization")).map(value -> (Boolean) value));
154+
eventActions.setStateDelta(
155+
actionsMap.get("stateDelta") != null
156+
? new ConcurrentHashMap<>((Map<String, Object>) actionsMap.get("stateDelta"))
157+
: new ConcurrentHashMap<>());
158+
eventActions.setArtifactDelta(
159+
actionsMap.get("artifactDelta") != null
160+
? convertToArtifactDeltaMap(actionsMap.get("artifactDelta"))
161+
: new ConcurrentHashMap<>());
162+
eventActions.setTransferToAgent(
163+
actionsMap.get("transferAgent") != null
164+
? (String) actionsMap.get("transferAgent")
165+
: null);
166+
eventActions.setEscalate(
167+
Optional.ofNullable(actionsMap.get("escalate")).map(value -> (Boolean) value));
168+
eventActions.setRequestedAuthConfigs(
169+
Optional.ofNullable(actionsMap.get("requestedAuthConfigs"))
170+
.map(SessionJsonConverter::asConcurrentMapOfConcurrentMaps)
171+
.orElse(new ConcurrentHashMap<>()));
172+
}
173+
174+
Event event =
175+
Event.builder()
176+
.id((String) Iterables.getLast(Splitter.on('/').split(apiEvent.get("name").toString())))
177+
.invocationId((String) apiEvent.get("invocationId"))
178+
.author((String) apiEvent.get("author"))
179+
.actions(eventActions)
180+
.content(
181+
Optional.ofNullable(apiEvent.get("content"))
182+
.map(SessionJsonConverter::convertMapToContent)
183+
.map(SessionUtils::decodeContent)
184+
.orElse(null))
185+
.timestamp(convertToInstant(apiEvent.get("timestamp")).toEpochMilli())
186+
.errorCode(
187+
Optional.ofNullable(apiEvent.get("errorCode"))
188+
.map(value -> new FinishReason((String) value)))
189+
.errorMessage(
190+
Optional.ofNullable(apiEvent.get("errorMessage")).map(value -> (String) value))
191+
.branch(Optional.ofNullable(apiEvent.get("branch")).map(value -> (String) value))
192+
.build();
193+
// TODO(b/414263934): Add Event branch and grounding metadata for python parity.
194+
if (apiEvent.get("eventMetadata") != null) {
195+
Map<String, Object> eventMetadata = (Map<String, Object>) apiEvent.get("eventMetadata");
196+
List<String> longRunningToolIdsList = (List<String>) eventMetadata.get("longRunningToolIds");
197+
198+
GroundingMetadata groundingMetadata = null;
199+
Object rawGroundingMetadata = eventMetadata.get("groundingMetadata");
200+
if (rawGroundingMetadata != null) {
201+
groundingMetadata =
202+
objectMapper.convertValue(rawGroundingMetadata, GroundingMetadata.class);
203+
}
204+
205+
event =
206+
event.toBuilder()
207+
.partial(Optional.ofNullable((Boolean) eventMetadata.get("partial")).orElse(false))
208+
.turnComplete(
209+
Optional.ofNullable((Boolean) eventMetadata.get("turnComplete")).orElse(false))
210+
.interrupted(
211+
Optional.ofNullable((Boolean) eventMetadata.get("interrupted")).orElse(false))
212+
.branch(Optional.ofNullable((String) eventMetadata.get("branch")))
213+
.groundingMetadata(groundingMetadata)
214+
.longRunningToolIds(
215+
longRunningToolIdsList != null ? new HashSet<>(longRunningToolIdsList) : null)
216+
.build();
217+
}
218+
return event;
219+
}
220+
221+
/**
222+
* Converts a timestamp from a Map or String into an {@link Instant}.
223+
*
224+
* @param timestampObj map with "seconds"/"nanos" or an ISO string.
225+
* @return parsed {@link Instant}.
226+
*/
227+
private static Instant convertToInstant(Object timestampObj) {
228+
if (timestampObj instanceof Map<?, ?> timestampMap) {
229+
return Instant.ofEpochSecond(
230+
((Number) timestampMap.get("seconds")).longValue(),
231+
((Number) timestampMap.get("nanos")).longValue());
232+
} else if (timestampObj != null) {
233+
return Instant.parse(timestampObj.toString());
234+
} else {
235+
throw new IllegalArgumentException("Timestamp not found in apiEvent");
236+
}
237+
}
238+
239+
/**
240+
* Converts a raw object from "artifactDelta" into a {@link ConcurrentMap} of {@link String} to
241+
* {@link Part}.
242+
*
243+
* @param artifactDeltaObj The raw object from which to parse the artifact delta.
244+
* @return A {@link ConcurrentMap} representing the artifact delta.
245+
*/
246+
@SuppressWarnings("unchecked")
247+
private static ConcurrentMap<String, Part> convertToArtifactDeltaMap(Object artifactDeltaObj) {
248+
if (!(artifactDeltaObj instanceof Map)) {
249+
return new ConcurrentHashMap<>();
250+
}
251+
ConcurrentMap<String, Part> artifactDeltaMap = new ConcurrentHashMap<>();
252+
Map<String, Map<String, Object>> rawMap = (Map<String, Map<String, Object>>) artifactDeltaObj;
253+
for (Map.Entry<String, Map<String, Object>> entry : rawMap.entrySet()) {
254+
try {
255+
Part part = objectMapper.convertValue(entry.getValue(), Part.class);
256+
artifactDeltaMap.put(entry.getKey(), part);
257+
} catch (IllegalArgumentException e) {
258+
logger.warn("Error converting artifactDelta value to Part for key: {}", entry.getKey(), e);
259+
}
260+
}
261+
return artifactDeltaMap;
262+
}
263+
264+
/**
265+
* Converts a nested map into a {@link ConcurrentMap} of {@link ConcurrentMap}s.
266+
*
267+
* @return thread-safe nested map.
268+
*/
269+
@SuppressWarnings("unchecked")
270+
private static ConcurrentMap<String, ConcurrentMap<String, Object>>
271+
asConcurrentMapOfConcurrentMaps(Object value) {
272+
return ((Map<String, Map<String, Object>>) value)
273+
.entrySet().stream()
274+
.collect(
275+
ConcurrentHashMap::new,
276+
(map, entry) -> map.put(entry.getKey(), new ConcurrentHashMap<>(entry.getValue())),
277+
ConcurrentHashMap::putAll);
278+
}
279+
}

0 commit comments

Comments
 (0)