diff --git a/core/src/main/java/com/google/adk/plugins/GlobalInstructionPlugin.java b/core/src/main/java/com/google/adk/plugins/GlobalInstructionPlugin.java new file mode 100644 index 000000000..1773bf701 --- /dev/null +++ b/core/src/main/java/com/google/adk/plugins/GlobalInstructionPlugin.java @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.adk.plugins; + +import com.google.adk.agents.CallbackContext; +import com.google.adk.models.LlmRequest; +import com.google.adk.models.LlmResponse; +import com.google.adk.utils.InstructionUtils; +import com.google.common.collect.ImmutableList; +import com.google.genai.types.Content; +import com.google.genai.types.GenerateContentConfig; +import com.google.genai.types.Part; +import io.reactivex.rxjava3.core.Maybe; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** + * Plugin that provides global instructions functionality at the App level. + * + *

Global instructions are applied to all agents in the application, providing a consistent way + * to set application-wide instructions, identity, or personality. Global instructions can be + * provided as a static string, or as a function that resolves the instruction based on the {@link + * CallbackContext}. + * + *

The plugin operates through the before_model_callback, allowing it to modify LLM requests + * before they are sent to the model by prepending the global instruction to any existing system + * instructions provided by the agent. + */ +public class GlobalInstructionPlugin extends BasePlugin { + + private final Function> instructionProvider; + + private static Function> createInstructionProvider( + String globalInstruction) { + return callbackContext -> { + if (globalInstruction == null) { + return Maybe.empty(); + } + return InstructionUtils.injectSessionState( + callbackContext.invocationContext(), globalInstruction) + .toMaybe(); + }; + } + + public GlobalInstructionPlugin(String globalInstruction) { + this(globalInstruction, "global_instruction"); + } + + public GlobalInstructionPlugin(String globalInstruction, String name) { + this(createInstructionProvider(globalInstruction), name); + } + + public GlobalInstructionPlugin(Function> instructionProvider) { + this(instructionProvider, "global_instruction"); + } + + public GlobalInstructionPlugin( + Function> instructionProvider, String name) { + super(name); + this.instructionProvider = instructionProvider; + } + + @Override + public Maybe beforeModelCallback( + CallbackContext callbackContext, LlmRequest.Builder llmRequest) { + return instructionProvider + .apply(callbackContext) + .filter(instruction -> !instruction.isEmpty()) + .flatMap( + instruction -> { + // Get mutable config, or create one if it doesn't exist. + GenerateContentConfig config = + llmRequest.config().orElseGet(GenerateContentConfig.builder()::build); + + // Get existing system instruction parts, if any. + Optional systemInstruction = config.systemInstruction(); + List existingParts = + systemInstruction.flatMap(Content::parts).orElse(ImmutableList.of()); + + // Prepend the global instruction to the existing system instruction parts. + // If there are existing instructions, add two newlines between the global + // instruction and the existing instructions. + ImmutableList.Builder newPartsBuilder = ImmutableList.builder(); + if (existingParts.isEmpty()) { + newPartsBuilder.add(Part.fromText(instruction)); + } else { + newPartsBuilder.add(Part.fromText(instruction + "\n\n")); + newPartsBuilder.addAll(existingParts); + } + + // Build the new system instruction content. + Content.Builder newSystemInstructionBuilder = Content.builder(); + systemInstruction.flatMap(Content::role).ifPresent(newSystemInstructionBuilder::role); + newSystemInstructionBuilder.parts(newPartsBuilder.build()); + + // Update llmRequest with new config. + llmRequest.config( + config.toBuilder() + .systemInstruction(newSystemInstructionBuilder.build()) + .build()); + return Maybe.empty(); + }); + } +} diff --git a/core/src/test/java/com/google/adk/plugins/GlobalInstructionPluginTest.java b/core/src/test/java/com/google/adk/plugins/GlobalInstructionPluginTest.java new file mode 100644 index 000000000..345314256 --- /dev/null +++ b/core/src/test/java/com/google/adk/plugins/GlobalInstructionPluginTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.adk.plugins; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import com.google.adk.agents.CallbackContext; +import com.google.adk.agents.InvocationContext; +import com.google.adk.artifacts.BaseArtifactService; +import com.google.adk.models.LlmRequest; +import com.google.adk.sessions.Session; +import com.google.adk.sessions.State; +import com.google.genai.types.Content; +import com.google.genai.types.GenerateContentConfig; +import com.google.genai.types.Part; +import io.reactivex.rxjava3.core.Maybe; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class GlobalInstructionPluginTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + @Mock private CallbackContext mockCallbackContext; + @Mock private InvocationContext mockInvocationContext; + private final State state = new State(new ConcurrentHashMap<>()); + private final Session session = Session.builder("session_id").state(state).build(); + @Mock private BaseArtifactService mockArtifactService; + + @Before + public void setUp() { + state.clear(); + when(mockCallbackContext.invocationId()).thenReturn("invocation_id"); + when(mockCallbackContext.agentName()).thenReturn("agent_name"); + when(mockCallbackContext.invocationContext()).thenReturn(mockInvocationContext); + when(mockInvocationContext.session()).thenReturn(session); + when(mockInvocationContext.artifactService()).thenReturn(mockArtifactService); + } + + @Test + public void beforeModelCallback_noExistingInstruction() { + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder(); + GlobalInstructionPlugin plugin = new GlobalInstructionPlugin("global instruction"); + plugin.beforeModelCallback(mockCallbackContext, llmRequestBuilder).test().assertComplete(); + Content systemInstruction = llmRequestBuilder.build().config().get().systemInstruction().get(); + List parts = systemInstruction.parts().get(); + assertThat(parts).hasSize(1); + assertThat(parts.get(0).text()).hasValue("global instruction"); + assertThat(systemInstruction.role()).isEmpty(); + } + + @Test + public void beforeModelCallback_withExistingInstruction() { + LlmRequest.Builder llmRequestBuilder = + LlmRequest.builder() + .config( + GenerateContentConfig.builder() + .systemInstruction( + Content.builder().parts(Part.fromText("existing instruction")).build()) + .build()); + GlobalInstructionPlugin plugin = new GlobalInstructionPlugin("global instruction"); + plugin.beforeModelCallback(mockCallbackContext, llmRequestBuilder).test().assertComplete(); + Content systemInstruction = llmRequestBuilder.build().config().get().systemInstruction().get(); + List parts = systemInstruction.parts().get(); + assertThat(parts).hasSize(2); + assertThat(parts.get(0).text()).hasValue("global instruction\n\n"); + assertThat(parts.get(1).text()).hasValue("existing instruction"); + assertThat(systemInstruction.role()).isEmpty(); + } + + @Test + public void beforeModelCallback_withInstructionProvider() { + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder(); + GlobalInstructionPlugin plugin = + new GlobalInstructionPlugin(unusedContext -> Maybe.just("instruction from provider")); + plugin.beforeModelCallback(mockCallbackContext, llmRequestBuilder).test().assertComplete(); + Content systemInstruction = llmRequestBuilder.build().config().get().systemInstruction().get(); + List parts = systemInstruction.parts().get(); + assertThat(parts).hasSize(1); + assertThat(parts.get(0).text()).hasValue("instruction from provider"); + assertThat(systemInstruction.role()).isEmpty(); + } + + @Test + public void beforeModelCallback_withStringInstruction_injectsState() { + state.put("name", "Alice"); + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder(); + GlobalInstructionPlugin plugin = new GlobalInstructionPlugin("Hello {name}"); + plugin.beforeModelCallback(mockCallbackContext, llmRequestBuilder).test().assertComplete(); + Content systemInstruction = llmRequestBuilder.build().config().get().systemInstruction().get(); + List parts = systemInstruction.parts().get(); + assertThat(parts).hasSize(1); + assertThat(parts.get(0).text()).hasValue("Hello Alice"); + assertThat(systemInstruction.role()).isEmpty(); + } + + @Test + public void beforeModelCallback_nullInstruction() { + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder(); + GlobalInstructionPlugin plugin = new GlobalInstructionPlugin((String) null); + plugin.beforeModelCallback(mockCallbackContext, llmRequestBuilder).test().assertComplete(); + assertThat(llmRequestBuilder.build().config()).isEmpty(); + } + + @Test + public void beforeModelCallback_emptyInstruction() { + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder(); + GlobalInstructionPlugin plugin = new GlobalInstructionPlugin(""); + plugin.beforeModelCallback(mockCallbackContext, llmRequestBuilder).test().assertComplete(); + assertThat(llmRequestBuilder.build().config()).isEmpty(); + } + + @Test + public void beforeModelCallback_withExistingInstructionAndRole_preservesRole() { + LlmRequest.Builder llmRequestBuilder = + LlmRequest.builder() + .config( + GenerateContentConfig.builder() + .systemInstruction( + Content.builder() + .parts(Part.fromText("existing instruction")) + .role("system") + .build()) + .build()); + GlobalInstructionPlugin plugin = new GlobalInstructionPlugin("global instruction"); + plugin.beforeModelCallback(mockCallbackContext, llmRequestBuilder).test().assertComplete(); + Content systemInstruction = llmRequestBuilder.build().config().get().systemInstruction().get(); + List parts = systemInstruction.parts().get(); + assertThat(parts).hasSize(2); + assertThat(parts.get(0).text()).hasValue("global instruction\n\n"); + assertThat(parts.get(1).text()).hasValue("existing instruction"); + assertThat(systemInstruction.role()).hasValue("system"); + } +}