diff --git a/core/src/main/java/com/google/adk/apps/App.java b/core/src/main/java/com/google/adk/apps/App.java index db1dc9b7a..385ae35b9 100644 --- a/core/src/main/java/com/google/adk/apps/App.java +++ b/core/src/main/java/com/google/adk/apps/App.java @@ -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; @@ -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 plugins = ImmutableList.of(); @Nullable private EventsCompactionConfig eventsCompactionConfig; @@ -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; } @@ -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; } @@ -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); } diff --git a/core/src/test/java/com/google/adk/apps/AppTest.java b/core/src/test/java/com/google/adk/apps/AppTest.java index 7202e2233..c76d19b81 100644 --- a/core/src/test/java/com/google/adk/apps/AppTest.java +++ b/core/src/test/java/com/google/adk/apps/AppTest.java @@ -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; @@ -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); + } }