Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions core/src/main/java/com/google/adk/apps/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package com.google.adk.apps;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.ContextCacheConfig;
import com.google.adk.agents.NodeViewAgent;
Expand Down Expand Up @@ -105,10 +103,30 @@ public ContextCacheConfig contextCacheConfig() {
return resumabilityConfig;
}

/**
* Returns a builder holding this app's settings. For an app that runs a node, the builder holds
* {@link #rootNode()} rather than the agent that stands in for it. To switch between a root agent
* and a root node, also clear the current one with {@code null}.
*/
public Builder toBuilder() {
Builder builder = new Builder();
builder.name = name;
if (rootNode != null) {
builder.rootNode = rootNode;
} else {
builder.rootAgent = rootAgent;
}
builder.plugins = plugins;
builder.eventsCompactionConfig = eventsCompactionConfig;
builder.contextCacheConfig = contextCacheConfig;
builder.resumabilityConfig = resumabilityConfig;
return builder;
}

/** Builder for {@link App}. */
public static class Builder {
private String name;
private BaseAgent rootAgent;
private @Nullable BaseAgent rootAgent;
private @Nullable Node rootNode;
private List<? extends Plugin> plugins = ImmutableList.of();
@Nullable private EventsCompactionConfig eventsCompactionConfig;
Expand All @@ -121,20 +139,21 @@ public Builder name(String name) {
return this;
}

/** Sets the root agent, or clears it when {@code null}. */
@CanIgnoreReturnValue
public Builder rootAgent(BaseAgent rootAgent) {
public Builder rootAgent(@Nullable BaseAgent rootAgent) {
this.rootAgent = rootAgent;
return this;
}

/**
* Sets the workflow node the app runs in place of a root agent. If the given node is an agent,
* it becomes the root agent.
* Sets the workflow node the app runs in place of a root agent, or clears it when {@code null}.
* If the given node is an agent, it becomes the root agent.
*/
@CanIgnoreReturnValue
@Experimental
public Builder rootNode(Node rootNode) {
this.rootNode = checkNotNull(rootNode);
public Builder rootNode(@Nullable Node rootNode) {
this.rootNode = rootNode;
return this;
}

Expand All @@ -151,20 +170,20 @@ public Builder plugins(Plugin... plugins) {
}

@CanIgnoreReturnValue
public Builder eventsCompactionConfig(EventsCompactionConfig eventsCompactionConfig) {
public Builder eventsCompactionConfig(@Nullable EventsCompactionConfig eventsCompactionConfig) {
this.eventsCompactionConfig = eventsCompactionConfig;
return this;
}

@CanIgnoreReturnValue
public Builder contextCacheConfig(ContextCacheConfig contextCacheConfig) {
public Builder contextCacheConfig(@Nullable ContextCacheConfig contextCacheConfig) {
this.contextCacheConfig = contextCacheConfig;
return this;
}

@CanIgnoreReturnValue
@Experimental
public Builder resumabilityConfig(ResumabilityConfig resumabilityConfig) {
public Builder resumabilityConfig(@Nullable ResumabilityConfig resumabilityConfig) {
this.resumabilityConfig = resumabilityConfig;
return this;
}
Expand Down Expand Up @@ -200,7 +219,8 @@ private BaseAgent resolveRootAgent() {
return rootAgent;
}
if (rootAgent != null) {
throw new IllegalStateException("Set a root agent or a root node, not both.");
throw new IllegalStateException(
"Set a root agent or a root node, not both; clear the other with null.");
}
return rootNode instanceof BaseAgent agent ? agent : new NodeViewAgent(rootNode);
}
Expand Down
102 changes: 102 additions & 0 deletions core/src/test/java/com/google/adk/apps/AppTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
import static org.junit.Assert.assertThrows;

import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.ContextCacheConfig;
import com.google.adk.agents.NodeViewAgent;
import com.google.adk.plugins.BasePlugin;
import com.google.adk.plugins.Plugin;
import com.google.adk.summarizer.EventsCompactionConfig;
import com.google.adk.testing.TestNode;
import com.google.adk.testing.TestUtils;
import com.google.adk.workflow.Node;
Expand Down Expand Up @@ -95,4 +99,102 @@ public void build_withANodeViewAgentAsTheRootNode_throwsIllegalStateException()

assertThrows(IllegalStateException.class, builder::build);
}

@Test
public void toBuilder_unchanged_buildsAnAppWithTheSameSettings() {
BaseAgent agent = TestUtils.createSubAgent("worker");
Plugin plugin = new BasePlugin("plugin") {};
EventsCompactionConfig compaction = new EventsCompactionConfig(3, 1);
ContextCacheConfig cache = new ContextCacheConfig();
ResumabilityConfig resumability = ResumabilityConfig.builder().resumable(true).build();
App app =
App.builder()
.name("agent_app")
.rootAgent(agent)
.plugins(plugin)
.eventsCompactionConfig(compaction)
.contextCacheConfig(cache)
.resumabilityConfig(resumability)
.build();

App copy = app.toBuilder().build();

assertThat(copy.name()).isEqualTo("agent_app");
assertThat(copy.rootAgent()).isSameInstanceAs(agent);
assertThat(copy.rootNode()).isEmpty();
assertThat(copy.plugins()).containsExactly(plugin);
assertThat(copy.eventsCompactionConfig()).isSameInstanceAs(compaction);
assertThat(copy.contextCacheConfig()).isSameInstanceAs(cache);
assertThat(copy.resumabilityConfig()).isSameInstanceAs(resumability);
}

@Test
public void toBuilder_ofANodeApp_keepsTheRootNode() {
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().build();

assertThat(copy.rootNode()).hasValue(NODE);
assertThat(((NodeViewAgent) copy.rootAgent()).node()).isSameInstanceAs(NODE);
}

@Test
public void toBuilder_changed_buildsAnAppWithTheChanges() {
Plugin plugin = new BasePlugin("plugin") {};
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().name("renamed").plugins(plugin).build();

assertThat(copy.name()).isEqualTo("renamed");
assertThat(copy.plugins()).containsExactly(plugin);
assertThat(copy.rootNode()).hasValue(NODE);
}

@Test
public void toBuilder_ofANodeAppClearingTheNodeAndSettingAnAgent_runsTheAgent() {
BaseAgent agent = TestUtils.createSubAgent("worker");
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().rootNode(null).rootAgent(agent).build();

assertThat(copy.rootAgent()).isSameInstanceAs(agent);
assertThat(copy.rootNode()).isEmpty();
}

@Test
public void toBuilder_ofAnAgentAppClearingTheAgentAndSettingANode_runsTheNode() {
App app = App.builder().name("agent_app").rootAgent(TestUtils.createSubAgent("worker")).build();

App copy = app.toBuilder().rootAgent(null).rootNode(NODE).build();

assertThat(copy.rootNode()).hasValue(NODE);
assertThat(((NodeViewAgent) copy.rootAgent()).node()).isSameInstanceAs(NODE);
}

@Test
public void toBuilder_ofANodeAppSettingAnotherNode_standsInForTheNewNode() {
Node other = TestNode.of("other");
App app = App.builder().name("node_app").rootNode(NODE).build();

App copy = app.toBuilder().rootNode(other).build();

assertThat(copy.rootNode()).hasValue(other);
assertThat(((NodeViewAgent) copy.rootAgent()).node()).isSameInstanceAs(other);
}

@Test
public void toBuilder_ofANodeAppClearingTheNode_throwsIllegalStateException() {
App app = App.builder().name("node_app").rootNode(NODE).build();
App.Builder builder = app.toBuilder().rootNode(null);

assertThrows(IllegalStateException.class, builder::build);
}

@Test
public void toBuilder_ofANodeAppSettingAnAgent_throwsIllegalStateException() {
App app = App.builder().name("node_app").rootNode(NODE).build();
App.Builder builder = app.toBuilder().rootAgent(TestUtils.createSubAgent("worker"));

assertThrows(IllegalStateException.class, builder::build);
}
}
Loading