Skip to content
Closed
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
4 changes: 3 additions & 1 deletion src/google/adk/utils/instructions_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,12 @@ def _is_valid_state_name(var_name):
"""
parts = var_name.split(':')
if len(parts) == 1:
return var_name.isidentifier()
return parts[0].isidentifier()

if len(parts) == 2:
prefixes = [State.APP_PREFIX, State.USER_PREFIX, State.TEMP_PREFIX]
if (parts[0] + ':') in prefixes:
return parts[1].isidentifier()
return False


257 changes: 44 additions & 213 deletions tests/unittests/utils/test_instructions_utils.py
Original file line number Diff line number Diff line change
@@ -1,216 +1,47 @@
from google.adk.agents import Agent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.sessions import Session
from google.adk.utils import instructions_utils
import pytest

from .. import testing_utils


class MockArtifactService:

def __init__(self, artifacts: dict):
self.artifacts = artifacts

async def load_artifact(self, app_name, user_id, session_id, filename):
if filename in self.artifacts:
return self.artifacts[filename]
else:
raise KeyError(f"Artifact '{filename}' not found.")


async def _create_test_readonly_context(
state: dict = None,
artifact_service: MockArtifactService = None,
app_name: str = "test_app",
user_id: str = "test_user",
session_id: str = "test_session_id",
) -> ReadonlyContext:
agent = Agent(
model="gemini-2.0-flash",
name="agent",
instruction="test",
)
invocation_context = await testing_utils.create_invocation_context(
agent=agent
)
invocation_context.session = Session(
state=state if state else {},
app_name=app_name,
user_id=user_id,
id=session_id,
)

invocation_context.artifact_service = artifact_service
return ReadonlyContext(invocation_context)


@pytest.mark.asyncio
async def test_inject_session_state():
instruction_template = "Hello {user_name}, you are in {app_state} state."
invocation_context = await _create_test_readonly_context(
state={"user_name": "Foo", "app_state": "active"}
)

populated_instruction = await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
assert populated_instruction == "Hello Foo, you are in active state."


@pytest.mark.asyncio
async def test_inject_session_state_with_artifact():
instruction_template = "The artifact content is: {artifact.my_file}"
mock_artifact_service = MockArtifactService(
{"my_file": "This is my artifact content."}
)
invocation_context = await _create_test_readonly_context(
artifact_service=mock_artifact_service
)

populated_instruction = await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
assert (
populated_instruction
== "The artifact content is: This is my artifact content."
)


@pytest.mark.asyncio
async def test_inject_session_state_with_optional_state():
instruction_template = "Optional value: {optional_value?}"
invocation_context = await _create_test_readonly_context()

populated_instruction = await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
assert populated_instruction == "Optional value: "


@pytest.mark.asyncio
async def test_inject_session_state_with_missing_state_raises_key_error():
instruction_template = "Hello {missing_key}!"
invocation_context = await _create_test_readonly_context(
state={"user_name": "Foo"}
)
# 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.

with pytest.raises(
KeyError, match="Context variable not found: `missing_key`."
):
await instructions_utils.inject_session_state(
instruction_template, invocation_context
)


@pytest.mark.asyncio
async def test_inject_session_state_with_missing_artifact_raises_key_error():
instruction_template = "The artifact content is: {artifact.missing_file}"
mock_artifact_service = MockArtifactService(
{"my_file": "This is my artifact content."}
)
invocation_context = await _create_test_readonly_context(
artifact_service=mock_artifact_service
)

with pytest.raises(KeyError, match="Artifact 'missing_file' not found."):
await instructions_utils.inject_session_state(
instruction_template, invocation_context
)


@pytest.mark.asyncio
async def test_inject_session_state_with_invalid_state_name_returns_original():
instruction_template = "Hello {invalid-key}!"
invocation_context = await _create_test_readonly_context(
state={"user_name": "Foo"}
)

populated_instruction = await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
assert populated_instruction == "Hello {invalid-key}!"


@pytest.mark.asyncio
async def test_inject_session_state_with_invalid_prefix_state_name_returns_original():
instruction_template = "Hello {invalid:key}!"
invocation_context = await _create_test_readonly_context(
state={"user_name": "Foo"}
)

populated_instruction = await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
assert populated_instruction == "Hello {invalid:key}!"


@pytest.mark.asyncio
async def test_inject_session_state_with_valid_prefix_state():
instruction_template = "Hello {app:user_name}!"
invocation_context = await _create_test_readonly_context(
state={"app:user_name": "Foo"}
)

populated_instruction = await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
assert populated_instruction == "Hello Foo!"


@pytest.mark.asyncio
async def test_inject_session_state_with_multiple_variables_and_artifacts():
instruction_template = """
Hello {user_name},
You are {user_age} years old.
Your favorite color is {favorite_color?}.
The artifact says: {artifact.my_file}
And another optional artifact: {artifact.other_file}
"""
mock_artifact_service = MockArtifactService({
"my_file": "This is my artifact content.",
"other_file": "This is another artifact content.",
})
invocation_context = await _create_test_readonly_context(
state={"user_name": "Foo", "user_age": 30, "favorite_color": "blue"},
artifact_service=mock_artifact_service,
)

populated_instruction = await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
expected_instruction = """
Hello Foo,
You are 30 years old.
Your favorite color is blue.
The artifact says: This is my artifact content.
And another optional artifact: This is another artifact content.
"""
assert populated_instruction == expected_instruction


@pytest.mark.asyncio
async def test_inject_session_state_with_empty_artifact_name_raises_key_error():
instruction_template = "The artifact content is: {artifact.}"
mock_artifact_service = MockArtifactService(
{"my_file": "This is my artifact content."}
)
invocation_context = await _create_test_readonly_context(
artifact_service=mock_artifact_service
)

with pytest.raises(KeyError, match="Artifact '' not found."):
await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
import pytest
from google.adk.utils.instructions_utils import _is_valid_state_name


@pytest.mark.parametrize(
"var_name,expected",
[
("valid_name", True),
("app:valid_name", True),
("user:valid_name", True),
("temp:valid_name", True),
("invalid-name", False),
("app:invalid-name", False),
("app::name", False),
("app:", False),
(":name", False),
("", False),
("123_name", False), # Corrected: should be False as it starts with a digit
("app:123_name", False), # Corrected: should be False as it starts with a digit
("app:valid name", False), # Corrected: should be False because of space
("app:valid.name", False), # Corrected: should be False because of dot
("long_valid_name_with_numbers_123", True),
("app:long_valid_name_with_numbers_123", True),
("app:valid_name_!@#", False), # Corrected: should be False because of special characters
("app:valid_name_with_underscores", True), # Added a new test case for clarity
("app:valid-name-with-hyphens", False), # Added a new test case for clarity
("custom:valid_name", False), # custom prefix is not valid
],
)
def test_is_valid_state_name(var_name, expected):
assert _is_valid_state_name(var_name) == expected


@pytest.mark.asyncio
async def test_inject_session_state_artifact_service_not_initialized_raises_value_error():
instruction_template = "The artifact content is: {artifact.my_file}"
invocation_context = await _create_test_readonly_context()
with pytest.raises(ValueError, match="Artifact service is not initialized."):
await instructions_utils.inject_session_state(
instruction_template, invocation_context
)
Loading