Skip to content

Commit 6cbec1c

Browse files
Merge branch 'main' into main
2 parents 89b3ed7 + f6ff2f2 commit 6cbec1c

15 files changed

+869
-29
lines changed

core/src/main/java/com/google/adk/events/EventActions.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class EventActions {
4141
private ConcurrentMap<String, ToolConfirmation> requestedToolConfirmations =
4242
new ConcurrentHashMap<>();
4343
private Optional<Boolean> endInvocation = Optional.empty();
44+
private Optional<EventCompaction> compaction = Optional.empty();
4445

4546
/** Default constructor for Jackson. */
4647
public EventActions() {}
@@ -139,6 +140,15 @@ public void setEndInvocation(boolean endInvocation) {
139140
this.endInvocation = Optional.of(endInvocation);
140141
}
141142

143+
@JsonProperty("compaction")
144+
public Optional<EventCompaction> compaction() {
145+
return compaction;
146+
}
147+
148+
public void setCompaction(Optional<EventCompaction> compaction) {
149+
this.compaction = compaction;
150+
}
151+
142152
public static Builder builder() {
143153
return new Builder();
144154
}
@@ -162,7 +172,8 @@ public boolean equals(Object o) {
162172
&& Objects.equals(escalate, that.escalate)
163173
&& Objects.equals(requestedAuthConfigs, that.requestedAuthConfigs)
164174
&& Objects.equals(requestedToolConfirmations, that.requestedToolConfirmations)
165-
&& Objects.equals(endInvocation, that.endInvocation);
175+
&& Objects.equals(endInvocation, that.endInvocation)
176+
&& Objects.equals(compaction, that.compaction);
166177
}
167178

168179
@Override
@@ -175,7 +186,8 @@ public int hashCode() {
175186
escalate,
176187
requestedAuthConfigs,
177188
requestedToolConfirmations,
178-
endInvocation);
189+
endInvocation,
190+
compaction);
179191
}
180192

181193
/** Builder for {@link EventActions}. */
@@ -190,6 +202,7 @@ public static class Builder {
190202
private ConcurrentMap<String, ToolConfirmation> requestedToolConfirmations =
191203
new ConcurrentHashMap<>();
192204
private Optional<Boolean> endInvocation = Optional.empty();
205+
private Optional<EventCompaction> compaction = Optional.empty();
193206

194207
public Builder() {}
195208

@@ -203,6 +216,7 @@ private Builder(EventActions eventActions) {
203216
this.requestedToolConfirmations =
204217
new ConcurrentHashMap<>(eventActions.requestedToolConfirmations());
205218
this.endInvocation = eventActions.endInvocation();
219+
this.compaction = eventActions.compaction();
206220
}
207221

208222
@CanIgnoreReturnValue
@@ -262,6 +276,13 @@ public Builder endInvocation(boolean endInvocation) {
262276
return this;
263277
}
264278

279+
@CanIgnoreReturnValue
280+
@JsonProperty("compaction")
281+
public Builder compaction(EventCompaction value) {
282+
this.compaction = Optional.ofNullable(value);
283+
return this;
284+
}
285+
265286
@CanIgnoreReturnValue
266287
public Builder merge(EventActions other) {
267288
if (other.skipSummarization().isPresent()) {
@@ -288,6 +309,9 @@ public Builder merge(EventActions other) {
288309
if (other.endInvocation().isPresent()) {
289310
this.endInvocation = other.endInvocation();
290311
}
312+
if (other.compaction().isPresent()) {
313+
this.compaction = other.compaction();
314+
}
291315
return this;
292316
}
293317

@@ -301,6 +325,7 @@ public EventActions build() {
301325
eventActions.setRequestedAuthConfigs(this.requestedAuthConfigs);
302326
eventActions.setRequestedToolConfirmations(this.requestedToolConfirmations);
303327
eventActions.setEndInvocation(this.endInvocation);
328+
eventActions.setCompaction(this.compaction);
304329
return eventActions;
305330
}
306331
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.google.adk.events;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
6+
import com.google.auto.value.AutoValue;
7+
import com.google.genai.types.Content;
8+
9+
/** The compaction of the events. */
10+
@AutoValue
11+
@JsonDeserialize(builder = EventCompaction.Builder.class)
12+
public abstract class EventCompaction {
13+
14+
@JsonProperty("startTimestamp")
15+
public abstract long startTimestamp();
16+
17+
@JsonProperty("endTimestamp")
18+
public abstract long endTimestamp();
19+
20+
@JsonProperty("compactedContent")
21+
public abstract Content compactedContent();
22+
23+
public static Builder builder() {
24+
return new AutoValue_EventCompaction.Builder();
25+
}
26+
27+
/** Builder for {@link EventCompaction}. */
28+
@AutoValue.Builder
29+
public abstract static class Builder {
30+
31+
@JsonCreator
32+
static Builder create() {
33+
return builder();
34+
}
35+
36+
@JsonProperty("startTimestamp")
37+
public abstract Builder startTimestamp(long startTimestamp);
38+
39+
@JsonProperty("endTimestamp")
40+
public abstract Builder endTimestamp(long endTimestamp);
41+
42+
@JsonProperty("compactedContent")
43+
public abstract Builder compactedContent(Content compactedContent);
44+
45+
public abstract EventCompaction build();
46+
}
47+
}

core/src/main/java/com/google/adk/sessions/InMemorySessionService.java

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import io.reactivex.rxjava3.core.Single;
2828
import java.time.Instant;
2929
import java.util.ArrayList;
30+
import java.util.Collection;
3031
import java.util.List;
3132
import java.util.Map;
3233
import java.util.Objects;
@@ -35,8 +36,6 @@
3536
import java.util.concurrent.ConcurrentHashMap;
3637
import java.util.concurrent.ConcurrentMap;
3738
import org.jspecify.annotations.Nullable;
38-
import org.slf4j.Logger;
39-
import org.slf4j.LoggerFactory;
4039

4140
/**
4241
* An in-memory implementation of {@link BaseSessionService} assuming {@link Session} objects are
@@ -50,9 +49,6 @@
5049
* during retrieval operations ({@code getSession}, {@code createSession}).
5150
*/
5251
public final class InMemorySessionService implements BaseSessionService {
53-
54-
private static final Logger logger = LoggerFactory.getLogger(InMemorySessionService.class);
55-
5652
// Structure: appName -> userId -> sessionId -> Session
5753
private final ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<String, Session>>>
5854
sessions;
@@ -178,9 +174,7 @@ public Single<ListSessionsResponse> listSessions(String appName, String userId)
178174

179175
// Create copies with empty events and state for the response
180176
List<Session> sessionCopies =
181-
userSessionsMap.values().stream()
182-
.map(this::copySessionMetadata)
183-
.collect(toCollection(ArrayList::new));
177+
prepareSessionsForListResponse(appName, userId, userSessionsMap.values());
184178

185179
return Single.just(ListSessionsResponse.builder().sessions(sessionCopies).build());
186180
}
@@ -298,21 +292,6 @@ private Session copySession(Session original) {
298292
.build();
299293
}
300294

301-
/**
302-
* Creates a copy of the session containing only metadata fields (ID, appName, userId, timestamp).
303-
* State and Events are explicitly *not* copied.
304-
*
305-
* @param original The session whose metadata to copy.
306-
* @return A new Session instance with only metadata fields populated.
307-
*/
308-
private Session copySessionMetadata(Session original) {
309-
return Session.builder(original.id())
310-
.appName(original.appName())
311-
.userId(original.userId())
312-
.lastUpdateTime(original.lastUpdateTime())
313-
.build();
314-
}
315-
316295
/**
317296
* Merges the app-specific and user-specific state into the provided *mutable* session's state
318297
* map.
@@ -338,4 +317,24 @@ private Session mergeWithGlobalState(String appName, String userId, Session sess
338317

339318
return session;
340319
}
320+
321+
/**
322+
* Prepares copies of sessions for use in a {@code listSessions} response.
323+
*
324+
* <p>For each session provided, this method creates a deep copy, clears the copy's event list,
325+
* and merges app-level and user-level state into the copy's state map.
326+
*
327+
* @param appName The application name.
328+
* @param userId The user ID.
329+
* @param sessions The collection of sessions to process.
330+
* @return A list of processed {@link Session} copies.
331+
*/
332+
private List<Session> prepareSessionsForListResponse(
333+
String appName, String userId, Collection<Session> sessions) {
334+
return sessions.stream()
335+
.map(this::copySession)
336+
.peek(s -> s.events().clear())
337+
.map(s -> mergeWithGlobalState(appName, userId, s))
338+
.collect(toCollection(ArrayList::new));
339+
}
341340
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.google.adk.summarizer;
2+
3+
import com.google.adk.events.Event;
4+
import io.reactivex.rxjava3.core.Maybe;
5+
import java.util.List;
6+
7+
/** Base interface for producing events summary. */
8+
public interface BaseEventSummarizer {
9+
10+
/**
11+
* Compact a list of events into a single event.
12+
*
13+
* <p>If compaction failed, return {@link Maybe#empty()}. Otherwise, compact into a content and
14+
* return it.
15+
*
16+
* <p>This method will summarize the events and return a new summary event indicating the range of
17+
* events it summarized.
18+
*
19+
* @param events Events to compact.
20+
* @return The new compacted event, or {@link Maybe#empty()} if no compaction happened.
21+
*/
22+
Maybe<Event> summarizeEvents(List<Event> events);
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.google.adk.summarizer;
2+
3+
import com.google.adk.events.Event;
4+
import com.google.adk.sessions.BaseSessionService;
5+
import com.google.adk.sessions.Session;
6+
import io.reactivex.rxjava3.core.Completable;
7+
8+
/** Base interface for compacting events. */
9+
public interface EventCompactor {
10+
11+
/**
12+
* Compacts events in the given session. If there is compaction happened, the new compaction event
13+
* will be appended to the given {@link BaseSessionService}.
14+
*
15+
* @param session the session containing the events to be compacted.
16+
* @param sessionService the session service for appending the new compaction event.
17+
* @return the {@link Event} containing the events summary.
18+
*/
19+
Completable compact(Session session, BaseSessionService sessionService);
20+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.google.adk.summarizer;
2+
3+
import java.util.Optional;
4+
5+
/**
6+
* Configuration for event compaction.
7+
*
8+
* @param compactionInterval The number of <b>new</b> user-initiated invocations that, once fully
9+
* represented in the session's events, will trigger a compaction.
10+
* @param overlapSize The number of preceding invocations to include from the end of the last
11+
* compacted range. This creates an overlap between consecutive compacted summaries, maintaining
12+
* context.
13+
* @param summarizer An optional event summarizer to use for compaction.
14+
*/
15+
public record EventsCompactionConfig(
16+
int compactionInterval, int overlapSize, Optional<BaseEventSummarizer> summarizer) {
17+
18+
public EventsCompactionConfig(int compactionInterval, int overlapSize) {
19+
this(compactionInterval, overlapSize, Optional.empty());
20+
}
21+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.google.adk.summarizer;
2+
3+
import static java.util.function.Predicate.not;
4+
import static java.util.stream.Collectors.joining;
5+
6+
import com.google.adk.events.Event;
7+
import com.google.adk.events.EventActions;
8+
import com.google.adk.events.EventCompaction;
9+
import com.google.adk.models.BaseLlm;
10+
import com.google.adk.models.LlmRequest;
11+
import com.google.common.collect.ImmutableList;
12+
import com.google.genai.types.Content;
13+
import com.google.genai.types.Part;
14+
import io.reactivex.rxjava3.core.Maybe;
15+
import java.util.List;
16+
import java.util.Optional;
17+
18+
/** An LLM-based event summarizer for sliding window compaction. */
19+
public final class LlmEventSummarizer implements BaseEventSummarizer {
20+
21+
private static final String DEFAULT_PROMPT_TEMPLATE =
22+
"""
23+
The following is a conversation history between a user and an AI \
24+
agent. Please summarize the conversation, focusing on key \
25+
information and decisions made, as well as any unresolved \
26+
questions or tasks. The summary should be concise and capture the \
27+
essence of the interaction.
28+
29+
{conversation_history}
30+
""";
31+
32+
private final BaseLlm baseLlm;
33+
private final String promptTemplate;
34+
35+
public LlmEventSummarizer(BaseLlm baseLlm) {
36+
this(baseLlm, DEFAULT_PROMPT_TEMPLATE);
37+
}
38+
39+
public LlmEventSummarizer(BaseLlm baseLlm, String promptTemplate) {
40+
this.baseLlm = baseLlm;
41+
this.promptTemplate = promptTemplate;
42+
}
43+
44+
@Override
45+
public Maybe<Event> summarizeEvents(List<Event> events) {
46+
if (events.isEmpty()) {
47+
return Maybe.empty();
48+
}
49+
50+
String conversationHistory = formatEventsForPrompt(events);
51+
String prompt = promptTemplate.replace("{conversation_history}", conversationHistory);
52+
53+
LlmRequest llmRequest =
54+
LlmRequest.builder()
55+
.model(baseLlm.model())
56+
.contents(
57+
ImmutableList.of(
58+
Content.builder()
59+
.role("user")
60+
.parts(ImmutableList.of(Part.fromText(prompt)))
61+
.build()))
62+
.build();
63+
64+
return baseLlm
65+
.generateContent(llmRequest, false)
66+
.firstElement()
67+
.flatMap(
68+
llmResponse ->
69+
Maybe.fromOptional(
70+
llmResponse
71+
.content()
72+
.map(content -> content.toBuilder().role("model").build())
73+
.map(
74+
summaryContent ->
75+
EventCompaction.builder()
76+
.startTimestamp(events.get(0).timestamp())
77+
.endTimestamp(events.get(events.size() - 1).timestamp())
78+
.compactedContent(summaryContent)
79+
.build())
80+
.map(
81+
compaction ->
82+
Event.builder()
83+
.author("user")
84+
.actions(EventActions.builder().compaction(compaction).build())
85+
.invocationId(Event.generateEventId())
86+
.build())));
87+
}
88+
89+
private String formatEventsForPrompt(List<Event> events) {
90+
return events.stream()
91+
.flatMap(
92+
event ->
93+
event.content().flatMap(Content::parts).stream()
94+
.flatMap(List::stream)
95+
.map(Part::text)
96+
.flatMap(Optional::stream)
97+
.filter(not(String::isEmpty))
98+
.map(text -> event.author() + ": " + text))
99+
.collect(joining("\\n"));
100+
}
101+
}

0 commit comments

Comments
 (0)