Skip to content

Conversation

@Jainish-S
Copy link

@Jainish-S Jainish-S commented Nov 22, 2025

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:
Previously, inject_session_state() only supported flat state access (e.g., {user_name}), preventing users from accessing nested properties within state objects. This limitation forced developers to either flatten their state structure or manually handle template replacement, reducing code readability and flexibility when working with complex, hierarchical state structures.

Solution:
Added support for nested state access in template injection using dot notation with optional chaining. The implementation adds a _get_nested_value() helper function that:

  • Traverses dot-separated paths through nested dictionaries and objects
  • Supports both dictionary access (__getitem__) and attribute access (getattr)
  • Handles optional chaining with ? operator for safe navigation
  • Returns empty strings for None values or missing optional paths
  • Raises KeyError for missing required paths
  • Maintains compatibility with existing prefixed state variables (app:, user:, temp:)

This solution was chosen because it:

  • Maintains backward compatibility with existing flat state access
  • Follows common patterns from JavaScript/TypeScript (optional chaining)
  • Provides clear error messages for debugging
  • Works seamlessly with both dictionary-based and object-based state

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Summary of pytest results:

$ uv run pytest ./tests/unittests/utils/test_instructions_utils.py -v
OUT

=========================================================================================================================================== test session starts ============================================================================================================================================
platform darwin -- Python 3.11.13, pytest-9.0.1, pluggy-1.6.0 -- /Users/jainish/os/adk-python/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /Users/jainish/os/adk-python
configfile: pyproject.toml
plugins: mock-3.15.1, langsmith-0.4.29, xdist-3.8.0, anyio-4.10.0, asyncio-1.3.0
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collected 27 items                                                                                                                                                                                                                                                                                         

tests/unittests/utils/test_instructions_utils.py::test_inject_session_state PASSED                                                                                                                                                                                                                   [  3%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_artifact PASSED                                                                                                                                                                                                     [  7%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_optional_state PASSED                                                                                                                                                                                               [ 11%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_missing_state_raises_key_error PASSED                                                                                                                                                                               [ 14%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_missing_artifact_raises_key_error PASSED                                                                                                                                                                            [ 18%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_invalid_state_name_returns_original PASSED                                                                                                                                                                          [ 22%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_invalid_prefix_state_name_returns_original PASSED                                                                                                                                                                   [ 25%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_valid_prefix_state PASSED                                                                                                                                                                                           [ 29%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_multiple_variables_and_artifacts PASSED                                                                                                                                                                             [ 33%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_empty_artifact_name_raises_key_error PASSED                                                                                                                                                                         [ 37%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_artifact_service_not_initialized_raises_value_error PASSED                                                                                                                                                               [ 40%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_optional_missing_artifact_returns_empty PASSED                                                                                                                                                                      [ 44%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_none_state_value_returns_empty PASSED                                                                                                                                                                               [ 48%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_optional_missing_state_returns_empty PASSED                                                                                                                                                                         [ 51%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_nested_dict_access PASSED                                                                                                                                                                                           [ 55%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_deep_nested_access PASSED                                                                                                                                                                                           [ 59%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_optional_nested_access_existing PASSED                                                                                                                                                                              [ 62%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_optional_nested_access_missing PASSED                                                                                                                                                                               [ 66%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_optional_nested_missing_root PASSED                                                                                                                                                                                 [ 70%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_nested_none_value PASSED                                                                                                                                                                                            [ 74%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_optional_nested_none_value PASSED                                                                                                                                                                                   [ 77%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_missing_nested_key_raises_error PASSED                                                                                                                                                                              [ 81%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_required_parent_missing_raises_error PASSED                                                                                                                                                                         [ 85%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_nested_and_prefixed_state PASSED                                                                                                                                                                                    [ 88%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_mixed_nested_and_flat_state PASSED                                                                                                                                                                                  [ 92%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_numeric_nested_values PASSED                                                                                                                                                                                        [ 96%]
tests/unittests/utils/test_instructions_utils.py::test_inject_session_state_with_nested_object_attribute_access PASSED                                                                                                                                                                               [100%]

======================== 27 passed in 0.89s ==========================

Added 12 comprehensive test cases covering:

  • Basic and deep nested dictionary access
  • Optional chaining with existing and missing values
  • None value handling in nested paths
  • Error handling for missing required keys
  • Prefixed state variables with nesting (app:, user:, temp:)
  • Mixed nested and flat state access patterns
  • Numeric nested values
  • Object attribute access vs dictionary access

Manual End-to-End (E2E) Tests: Created a sample agent to demonstrate the feature (located at contributing/samples/nested_state_agent/, not included in this PR). Setup:

cd contributing/samples/nested_state_agent
adk run .

Agent code:

import logging

from google.adk.agents import Agent
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.utils.instructions_utils import inject_session_state


def inject_nested_state(callback_context: CallbackContext):
  callback_context.state["user"] = {
      "name": "John",
      "profile": {"age": 24, "role": "Software Engineer"},
  }
  logging.info("State populated with nested user object.")


async def build_instruction(readonly_context: ReadonlyContext) -> str:
  template = (
      "Current user is {user?.name} and {user?.profile?.role}. Please greet"
      " them by name and designation."
  )
  return await inject_session_state(template, readonly_context)


root_agent = Agent(
    name="nested_state_agent",
    model="gemini-2.0-flash-lite",
    instruction=build_instruction,
    before_agent_callback=[inject_nested_state],
)

Expected behavior:

  • Agent receives instruction: "Current user is John and Software Engineer. Please greet them by name and designation."
  • Agent responds with greeting including the user's name and role
  • Missing fields with optional chaining (?) return empty strings instead of raising errors

Actual output:

INFO: State populated with nested user object.
Agent: Hello John, Software Engineer! How can I help you today?

Result: Nested state values correctly injected into instruction template

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Note: This PR re-implements the solution for issue #575. A previous implementation existed but was not merged due to merge conflicts. This is a fresh implementation with the same functionality. Feature highlights:

  • ✅ Backward compatible with existing flat state access
  • ✅ Supports deeply nested structures: {user.profile.settings.theme}
  • ✅ Safe navigation with ?: {user?.profile?.department?} returns "" if missing
  • ✅ Works with both dict and object attributes
  • ✅ Compatible with prefixed state: {app:config.api.endpoint}
  • ✅ Clear error messages for debugging required fields

Files changed:

  • src/google/adk/utils/instructions_utils.py - Core implementation (+92 lines)
  • tests/unittests/utils/test_instructions_utils.py - Test coverage (+278 lines)

Key improvements made:

  1. ✅ Followed the exact template structure with all required sections
  2. ✅ Filled in all checkboxes appropriately
  3. ✅ Included actual pytest output summary as requested
  4. ✅ Provided clear E2E testing instructions with expected vs actual output
  5. ✅ Added context about this being a re-implementation
  6. ✅ Used proper markdown formatting throughout
  7. ✅ Kept your example code but formatted it better within the E2E section
  8. ✅ Made the testing plan more detailed and actionable

Resolves google#575

Previously, inject_session_state only supported flat state access,
preventing users from accessing nested properties in state objects.
This limited the ability to work with complex, hierarchical state
structures.

This change adds support for:
- Nested dict/object access using dot notation: {user.profile.role}
- Optional chaining with ? operator: {user?.profile?.role?}
- Deep nesting and mixed dict/object access
- Compatible with existing prefixed state (app:, user:, temp:)

Example usage:

async def build_instruction(readonly_context: ReadonlyContext) -> str:
  template = (
      "Current user is {user?.name} and {user?.profile?.role}. "
      "Please greet them by name and designation."
  )
  return await inject_session_state(template, readonly_context)

# With state:
state = {
    "user": {
        "name": "John",
        "profile": {"role": "Software Engineer", "age": 30}
    }
}

# Result: "Current user is John and Software Engineer.
#          Please greet them by name and designation."

# Missing fields with optional chaining return empty strings
# instead of raising errors.

Includes comprehensive test coverage with 12 new test cases covering
nested access, optional chaining, error handling, and edge cases.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Jainish-S, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the inject_session_state utility by enabling the injection of nested state variables into instruction templates. This feature allows developers to work with complex, hierarchical state objects more naturally, improving code readability and flexibility. The implementation includes support for dot notation, optional chaining, and compatibility with various state structures, addressing a previous limitation that only allowed flat state access.

Highlights

  • Nested State Access: Introduced support for accessing deeply nested state variables within instruction templates using dot notation (e.g., {user.profile.role}).
  • Optional Chaining: Implemented optional chaining with the ? operator (e.g., {user?.profile?.department?}) to safely navigate potentially missing nested paths, returning an empty string instead of raising an error.
  • Flexible Access Methods: The new functionality works seamlessly with both dictionary-based (__getitem__) and object-based (getattr) state structures.
  • Backward Compatibility: The changes maintain full backward compatibility with existing flat state access patterns and prefixed state variables (e.g., {app:config.api.endpoint}).
  • Error Handling: Provides clear KeyError messages for debugging when a required (non-optional) nested path is not found.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Nov 22, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for accessing nested state variables in instruction templates, including support for optional chaining. The implementation is mostly solid, with a new _get_nested_value helper function that correctly handles nested access for both dictionaries and objects. The test coverage is extensive and covers many scenarios for the new feature. However, I've identified a high-severity issue in the error handling logic within inject_session_state that could lead to incorrect behavior by silently ignoring missing required variables. My review includes a specific comment with a suggested fix for this issue.

@adk-bot
Copy link
Collaborator

adk-bot commented Nov 22, 2025

Response from ADK Triaging Agent

Hello @Jainish-S, thank you for your contribution!

Before we can merge this PR, we need you to sign the Contributor License Agreement (CLA). The CLA check is currently failing. You can review and sign the agreement here: https://cla.developers.google.com/

Once the CLA is signed, we can proceed with the review. Thanks!

The previous implementation incorrectly treated entire paths as optional
if any segment contained '?', violating per-segment optional chaining
semantics. For example, {user.profile?} with missing 'user' would
incorrectly return empty string instead of raising an error.

Root cause: The catch block checked 'if '?' in full_path' to decide
whether to suppress KeyError. This was wrong because _get_nested_value()
already handles optional segments correctly by returning None for
missing optional parts. Any KeyError that propagates means a required
segment was missing and should be fatal.

Fix: Removed the conditional suppression logic. Now all KeyErrors from
_get_nested_value() are properly re-raised, maintaining correct
per-segment optional chaining semantics.

Examples:
- {user?.profile} with user missing → empty string (correct: user? is optional)
- {user.profile?} with user missing → KeyError (correct: user is required)
- {user?.profile?.role} with user missing → empty string (correct: chain stops at user?)

Added test case to verify required parent with optional child raises error.
@Jainish-S Jainish-S marked this pull request as draft November 23, 2025 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core [Component] This issue is related to the core interface and implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support templating of nested values in instruction template

2 participants