chore: deprecate global_instructions and make it a plugin instead

PiperOrigin-RevId: 814307563
This commit is contained in:
George Weale
2025-10-02 13:43:22 -07:00
committed by Copybara-Service
parent 29f18f4eea
commit f667c7445e
6 changed files with 375 additions and 1 deletions
+15
View File
@@ -27,6 +27,7 @@ from typing import Literal
from typing import Optional
from typing import Type
from typing import Union
import warnings
from google.genai import types
from pydantic import BaseModel
@@ -151,6 +152,10 @@ class LlmAgent(BaseAgent):
global_instruction: Union[str, InstructionProvider] = ''
"""Instructions for all the agents in the entire agent tree.
DEPRECATED: This field is deprecated and will be removed in a future version.
Use GlobalInstructionPlugin instead, which provides the same functionality
at the App level. See migration guide for details.
ONLY the global_instruction in root agent will take effect.
For example: use global_instruction to make all agents have a stable identity
@@ -431,6 +436,16 @@ class LlmAgent(BaseAgent):
bypass_state_injection: Whether the instruction is based on
InstructionProvider.
"""
# Issue deprecation warning if global_instruction is being used
if self.global_instruction:
warnings.warn(
'global_instruction field is deprecated and will be removed in a'
' future version. Use GlobalInstructionPlugin instead for the same'
' functionality at the App level. See migration guide for details.',
DeprecationWarning,
stacklevel=2,
)
if isinstance(self.global_instruction, str):
return self.global_instruction, False
else:
@@ -22,6 +22,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from google.genai import types
from ..sessions.session import Session
from .invocation_context import InvocationContext
@@ -52,3 +53,8 @@ class ReadonlyContext:
def state(self) -> MappingProxyType[str, Any]:
"""The state of the current session. READONLY field."""
return MappingProxyType(self._invocation_context.session.state)
@property
def session(self) -> Session:
"""The current session for this invocation."""
return self._invocation_context.session
@@ -67,7 +67,8 @@ class _InstructionsLlmRequestProcessor(BaseLlmRequestProcessor):
root_agent: BaseAgent = agent.root_agent
# Handle global instructions
# Handle global instructions (DEPRECATED - use GlobalInstructionPlugin instead)
# TODO: Remove this code block when global_instruction field is removed
if isinstance(root_agent, LlmAgent) and root_agent.global_instruction:
raw_si, bypass_state_injection = (
await root_agent.canonical_global_instruction(
@@ -0,0 +1,131 @@
# Copyright 2025 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.
from __future__ import annotations
import inspect
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.adk.plugins.base_plugin import BasePlugin
from google.adk.utils import instructions_utils
if TYPE_CHECKING:
from google.adk.agents.llm_agent import InstructionProvider
from google.adk.agents.llm_agent import LlmAgent
class GlobalInstructionPlugin(BasePlugin):
"""Plugin that provides global instructions functionality at the App level.
This plugin replaces the deprecated global_instruction field on LlmAgent.
Global instructions are applied to all agents in the application, providing
a consistent way to set application-wide instructions, identity, or
personality.
The plugin operates through the before_model_callback, allowing it to modify
LLM requests before they are sent to the model.
"""
def __init__(
self,
global_instruction: Union[str, InstructionProvider] = "",
name: str = "global_instruction",
) -> None:
"""Initialize the GlobalInstructionPlugin.
Args:
global_instruction: The instruction to apply globally. Can be a string or
an InstructionProvider function that takes ReadonlyContext and returns a
string (sync or async).
name: The name of the plugin (defaults to "global_instruction").
"""
super().__init__(name=name)
self.global_instruction = global_instruction
async def before_model_callback(
self, *, callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]:
"""Apply global instructions to the LLM request.
This callback is executed before each request is sent to the model,
allowing the plugin to inject global instructions into the request.
Args:
callback_context: The context for the current agent call.
llm_request: The prepared request object to be sent to the model.
Returns:
None to allow the LLM request to proceed normally.
"""
# Only process if we have a global instruction configured
if not self.global_instruction:
return None
# Resolve the global instruction (handle both string and InstructionProvider)
readonly_context = ReadonlyContext(callback_context.invocation_context)
final_global_instruction = await self._resolve_global_instruction(
readonly_context
)
if not final_global_instruction:
return None
# Make the global instruction the leading system instruction.
existing_instruction = llm_request.config.system_instruction
if not existing_instruction:
llm_request.config.system_instruction = final_global_instruction
return None
if isinstance(existing_instruction, str):
llm_request.config.system_instruction = (
f"{final_global_instruction}\n\n{existing_instruction}"
)
else: # It's an Iterable
# Convert to list to allow prepending
new_instruction_list = [final_global_instruction]
new_instruction_list.extend(list(existing_instruction))
llm_request.config.system_instruction = new_instruction_list
return None
async def _resolve_global_instruction(
self, readonly_context: ReadonlyContext
) -> str:
"""Resolve the global instruction, handling both string and InstructionProvider.
Args:
readonly_context: The readonly context for resolving instructions.
Returns:
The fully resolved and processed global instruction string, ready to use.
"""
if isinstance(self.global_instruction, str):
# For string instructions, apply state injection
return await instructions_utils.inject_session_state(
self.global_instruction, readonly_context
)
else:
# Handle InstructionProvider (callable)
# InstructionProvider already handles state internally, no injection needed
instruction = self.global_instruction(readonly_context)
if inspect.isawaitable(instruction):
instruction = await instruction
return instruction
+13
View File
@@ -0,0 +1,13 @@
# Copyright 2025 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.
@@ -0,0 +1,208 @@
# Copyright 2025 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.
from unittest.mock import Mock
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.invocation_context import InvocationContext
from google.adk.agents.llm_agent import Agent
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.models.llm_request import LlmRequest
from google.adk.plugins.global_instruction_plugin import GlobalInstructionPlugin
from google.adk.sessions.session import Session
from google.genai import types
import pytest
@pytest.mark.asyncio
async def test_global_instruction_plugin_with_string():
"""Test GlobalInstructionPlugin with a string global instruction."""
plugin = GlobalInstructionPlugin(
global_instruction=(
"You are a helpful assistant with a friendly personality."
)
)
# Create mock objects
mock_session = Session(
app_name="test_app", user_id="test_user", id="test_session", state={}
)
mock_invocation_context = Mock(spec=InvocationContext)
mock_invocation_context.session = mock_session
mock_callback_context = Mock(spec=CallbackContext)
mock_callback_context.invocation_context = mock_invocation_context
llm_request = LlmRequest(
model="gemini-1.5-flash",
config=types.GenerateContentConfig(system_instruction=""),
)
# Execute the plugin's before_model_callback
result = await plugin.before_model_callback(
callback_context=mock_callback_context, llm_request=llm_request
)
# Plugin should return None to allow normal processing
assert result is None
# System instruction should now contain the global instruction
assert (
"You are a helpful assistant with a friendly personality."
in llm_request.config.system_instruction
)
@pytest.mark.asyncio
async def test_global_instruction_plugin_with_instruction_provider():
"""Test GlobalInstructionPlugin with an InstructionProvider function."""
async def build_global_instruction(readonly_context: ReadonlyContext) -> str:
return f"You are assistant for user {readonly_context.session.user_id}."
plugin = GlobalInstructionPlugin(global_instruction=build_global_instruction)
# Create mock objects
mock_session = Session(
app_name="test_app", user_id="alice", id="test_session", state={}
)
mock_invocation_context = Mock(spec=InvocationContext)
mock_invocation_context.session = mock_session
mock_callback_context = Mock(spec=CallbackContext)
mock_callback_context.invocation_context = mock_invocation_context
llm_request = LlmRequest(
model="gemini-1.5-flash",
config=types.GenerateContentConfig(system_instruction=""),
)
# Execute the plugin's before_model_callback
result = await plugin.before_model_callback(
callback_context=mock_callback_context, llm_request=llm_request
)
# Plugin should return None to allow normal processing
assert result is None
# System instruction should contain the dynamically generated instruction
assert (
"You are assistant for user alice."
in llm_request.config.system_instruction
)
@pytest.mark.asyncio
async def test_global_instruction_plugin_empty_instruction():
"""Test GlobalInstructionPlugin with empty global instruction."""
plugin = GlobalInstructionPlugin(global_instruction="")
# Create mock objects
mock_session = Session(
app_name="test_app", user_id="test_user", id="test_session", state={}
)
mock_invocation_context = Mock(spec=InvocationContext)
mock_invocation_context.session = mock_session
mock_callback_context = Mock(spec=CallbackContext)
mock_callback_context.invocation_context = mock_invocation_context
llm_request = LlmRequest(
model="gemini-1.5-flash",
config=types.GenerateContentConfig(
system_instruction="Original instruction"
),
)
# Execute the plugin's before_model_callback
result = await plugin.before_model_callback(
callback_context=mock_callback_context, llm_request=llm_request
)
# Plugin should return None to allow normal processing
assert result is None
# System instruction should remain unchanged
assert llm_request.config.system_instruction == "Original instruction"
@pytest.mark.asyncio
async def test_global_instruction_plugin_leads_existing():
"""Test that GlobalInstructionPlugin prepends global instructions."""
plugin = GlobalInstructionPlugin(
global_instruction="You are a helpful assistant."
)
# Create mock objects
mock_session = Session(
app_name="test_app", user_id="test_user", id="test_session", state={}
)
mock_invocation_context = Mock(spec=InvocationContext)
mock_invocation_context.session = mock_session
mock_callback_context = Mock(spec=CallbackContext)
mock_callback_context.invocation_context = mock_invocation_context
llm_request = LlmRequest(
model="gemini-1.5-flash",
config=types.GenerateContentConfig(
system_instruction="Existing instructions."
),
)
# Execute the plugin's before_model_callback
result = await plugin.before_model_callback(
callback_context=mock_callback_context, llm_request=llm_request
)
# Plugin should return None to allow normal processing
assert result is None
# System instruction should contain global instruction before existing ones
expected = "You are a helpful assistant.\n\nExisting instructions."
assert llm_request.config.system_instruction == expected
@pytest.mark.asyncio
async def test_global_instruction_plugin_prepends_to_list():
"""Test GlobalInstructionPlugin prepends to a list of instructions."""
plugin = GlobalInstructionPlugin(global_instruction="Global instruction.")
mock_session = Session(
app_name="test_app", user_id="test_user", id="test_session", state={}
)
mock_invocation_context = Mock(spec=InvocationContext)
mock_invocation_context.session = mock_session
mock_callback_context = Mock(spec=CallbackContext)
mock_callback_context.invocation_context = mock_invocation_context
llm_request = LlmRequest(
model="gemini-1.5-flash",
config=types.GenerateContentConfig(
system_instruction=["Existing instruction."]
),
)
await plugin.before_model_callback(
callback_context=mock_callback_context, llm_request=llm_request
)
expected = ["Global instruction.", "Existing instruction."]
assert llm_request.config.system_instruction == expected