mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
chore: deprecate global_instructions and make it a plugin instead
PiperOrigin-RevId: 814307563
This commit is contained in:
committed by
Copybara-Service
parent
29f18f4eea
commit
f667c7445e
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user