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
118 changes: 118 additions & 0 deletions core/src/main/java/com/google/adk/plugins/GlobalInstructionPlugin.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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}.
*
* <p>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<CallbackContext, Maybe<String>> instructionProvider;

private static Function<CallbackContext, Maybe<String>> 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<CallbackContext, Maybe<String>> instructionProvider) {
this(instructionProvider, "global_instruction");
}

public GlobalInstructionPlugin(
Function<CallbackContext, Maybe<String>> instructionProvider, String name) {
super(name);
this.instructionProvider = instructionProvider;
}

@Override
public Maybe<LlmResponse> 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<Content> systemInstruction = config.systemInstruction();
List<Part> 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<Part> newPartsBuilder = ImmutableList.<Part>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.<LlmResponse>empty();
});
}
}
Original file line number Diff line number Diff line change
@@ -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<Part> 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<Part> 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<Part> 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<Part> 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<Part> 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");
}
}