Compare commits

..

69 Commits

Author SHA1 Message Date
Xiang (Sean) Zhou f159bd9c87 fix: Use str() to calculate fingerprint instead of json.dumps
This is to avoid serialization issue for some fields that are not json serializable.
meanwhile restructure the debug logs in context cache manager for better debugging potential issues.

PiperOrigin-RevId: 811182492
2025-09-24 22:14:40 -07:00
Ankur Sharma d48679582d feat: Populate AppDetails to each Invocation
AppDetails require two pieces of information:
1) Instructions
2) Tools

Both these pieces of information are gathered using the llm_request that was passed to the model. This approach, slightly invasive, ensures that we capture the "exact" instructions and tools that were given to the model.

PiperOrigin-RevId: 811180648
2025-09-24 22:06:56 -07:00
Google Team Member 2a2da0fe03 feat: Introduce OAuth2DiscoveryManager to fetch metadata needed for OAuth
This is the first step to bring ADK to compliance with MCP Authorization Spec.

PiperOrigin-RevId: 811177152
2025-09-24 21:53:48 -07:00
Ankur Sharma 5a485b01cd feat: Adds Rubric based final response evaluator
The evaluator uses a set of rubrics to assess the quality of the agent's final response.

PiperOrigin-RevId: 811154498
2025-09-24 20:30:51 -07:00
Ankur Sharma 01923a9227 feat: Data model for storing App Details and data model for steps
Details:
1. Data model for storing App Details (the agentic system)
As we move towards LLM as Judge metrics, we see that some of these metrics need information about the Agentic system that was used for inferencing. We add a data model to capture that.

2. Data model for Steps
We refine the concept of intermediate data. Previously it stored data in the form of a multiple lists, thereby losing out on the chronological information. This information is needed for some of the metrics. So we refine the concept of intermediate data as series of logical steps that an Agent Take.

PiperOrigin-RevId: 811122784
2025-09-24 18:41:38 -07:00
Xiang (Sean) Zhou 08f3b48305 chore: Add sample agent to test non-text content in static instruction
PiperOrigin-RevId: 810999310
2025-09-24 13:03:11 -07:00
Xuan Yang 6db096a3f4 chore: remove unsupported 'type': 'unknown' in test_common.py for fastapi 0.117.1
PiperOrigin-RevId: 810673476
2025-09-23 19:44:49 -07:00
Xiang (Sean) Zhou 47bd34ac28 chore: Fix the type annotation
PiperOrigin-RevId: 810611299
2025-09-23 15:50:19 -07:00
Xiang (Sean) Zhou ae5592e242 chore: Add tests for instruction provider and merge test_static_instructions.py to test_intructions.py
PiperOrigin-RevId: 810610507
2025-09-23 15:47:46 -07:00
Xiang (Sean) Zhou 61213ce4d4 feat: Support non-text content in static instruction
move them to user contents and reference them from instruction

PiperOrigin-RevId: 810587466
2025-09-23 15:36:15 -07:00
Xuan Yang e86ca5762a chore: remove internal TODO comment
PiperOrigin-RevId: 810583734
2025-09-23 15:36:06 -07:00
Google Team Member cbb609233b chore: Sample Spanner RAG agent that wraps search_tool
Also modified README to add instructions on when to use which tool.

PiperOrigin-RevId: 810563458
2025-09-23 15:35:57 -07:00
George Weale 657369cffe fix: Adds plugin to save artifacts for issue #2176
PiperOrigin-RevId: 810522939
2025-09-23 15:35:48 -07:00
Xiang (Sean) Zhou c944a12e31 chore: Remove query schema mode, as it doesn't perform well as embedded schema mode
PiperOrigin-RevId: 810517055
2025-09-23 15:35:40 -07:00
Xiang (Sean) Zhou 26990c2622 chore: Add sample agent to test static instruction
PiperOrigin-RevId: 810516925
2025-09-23 15:35:31 -07:00
Xiang (Sean) Zhou f2ce990867 chore: Add experimental annotation to GeminiContextCacheManager
PiperOrigin-RevId: 810503537
2025-09-23 15:35:22 -07:00
shsha4 86dea5b53a fix(mcp): Initialize tool_name_prefix in MCPToolse
Merge https://github.com/google/adk-python/pull/2823

Description
  This change introduces a tool_name_prefix attribute to McpToolset and McpToolsetConfig. This allows for adding a prefix to the
  names of all tools within the toolset, which can help avoid naming collisions and provide better organization.

  The implementation involves updating the McpToolset's __init__ and from_config methods to handle the new tool_name_prefix and
  adding the corresponding field to McpToolsetConfig.

  Testing Plan
  A new unit test file has been added to ensure the functionality works as expected.

   - `tests/unittests/tools/test_mcp_toolset.py`:
     - The test_mcp_toolset_with_prefix test case verifies that the tool_name_prefix is correctly applied to the tool names
       retrieved from the toolset.
     - All tests were run via pytest and passed.

  Related Issue
   - Closes #2814

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2823 from shsha4:fix/issue-2814 e8e5b0d6d5f406d3875faf2229a96701725b7a5e
PiperOrigin-RevId: 810500616
2025-09-23 15:35:12 -07:00
Xiang (Sean) Zhou 6ca2aee829 ADK changes
PiperOrigin-RevId: 810492858
2025-09-23 15:35:02 -07:00
Xuan Yang 374522197f ADK changes
PiperOrigin-RevId: 810223422
2025-09-23 15:34:53 -07:00
Google Team Member aef1ee97a5 fix: make a copy of the columns instead of modifying it in place
This avoid unintentional modifications, especially in the case of a wrapped tool.

PiperOrigin-RevId: 810175539
2025-09-23 15:34:43 -07:00
Xiang (Sean) Zhou 38bbde6d56 chore: Annotate CachePerformanceAnalyzer as experimental
PiperOrigin-RevId: 809434619
2025-09-23 15:34:34 -07:00
TanejaAnkisetty 78fd4803d5 chore: Set role to user if new_message doesn't have role in Runner.run_async()
Merge https://github.com/google/adk-python/pull/2458

**Summary**
Verifies that user-provided messages are always passed to the LLM as 'user' role, regardless of whether the role is explicitly set in types.Content. Before the current fix, if the LlmRequest from the user doesn't have the 'user' role, but has the user content, then the text is being replaced with the standard text - "Handle the requests as specified in the System Instruction." and the content from the user is completely ignored and not passed into the LLM.

**Code to replicate the problem**

```
from google.adk.agents import LlmAgent
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.genai.types import Content, Part
from google.adk.models.lite_llm import LiteLlm
from google.adk.models import LlmRequest
from google.genai import types
from pydantic import Field

import litellm
litellm._turn_on_debug()

import warnings
warnings.filterwarnings("ignore", category=UserWarning, message=".*InMemoryCredentialService.*")

import os
from dotenv import load_dotenv

# Load environment variables from the agent directory's .env file
load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

# Define agent with output_key
root_agent = LlmAgent(
    name="name_of_agent",
    model=LiteLlm(model="azure/gpt-4o-mini"),
    instruction="You are a customer agent to help the users with their concerns."
)

# --- Setup Runner and Session ---
app_name, user_id, session_id = "state_app", "user1", "session1"

session_service = InMemorySessionService()

runner = Runner(
    agent=root_agent,
    app_name=app_name,
    session_service=session_service
)

print(f"Runner created for agent '{runner.agent.name}'.")

session = await session_service.create_session(
    app_name=app_name,
    user_id=user_id,
    session_id=session_id
)

# --- Run the Agent ---

async def call_agent_async(query: str, runner, user_id, session_id):

    user_message = Content(parts=[Part(text=query)])

    async for event in runner.run_async(
        user_id=user_id,
        session_id=session_id,
        new_message=user_message
    ):
        print("event")
        print(f"  [Event]\n  Author: {event.author}\n  Type: {type(event).__name__}",
        f"\n  Final: {event.is_final_response()}\n  Content: {event.content}")

    return event

event = await call_agent_async("What is the capital of India.",runner=runner,user_id=user_id,session_id=session_id)
```
**Before the fix (current adk-python code output)**
```
00:29:24 - LiteLLM:DEBUG: utils.py:348 -

00:29:24 - LiteLLM:DEBUG: utils.py:348 - Request to litellm:
00:29:24 - LiteLLM:DEBUG: utils.py:348 - litellm.acompletion(model='azure/gpt-4o-mini', messages=[{'role': 'developer', 'content': 'You are a customer agent to help the users with their concerns.\n\nYou are an agent. Your internal name is "name_of_agent".'}, {'role': 'user', 'content': 'Handle the requests as specified in the System Instruction.'}], tools=None, response_format=None)
```

**After the fix (after resolving the fix)**
```
00:28:46 - LiteLLM:DEBUG: utils.py:349 -

00:28:46 - LiteLLM:DEBUG: utils.py:349 - Request to litellm:
00:28:46 - LiteLLM:DEBUG: utils.py:349 - litellm.acompletion(model='azure/gpt-4o-mini', messages=[{'role': 'developer', 'content': 'You are a customer agent to help the users with their concerns.\n\nYou are an agent. Your internal name is "name_of_agent".'}, {'role': 'user', 'content': 'What is the capital of India.'}], tools=None, response_format=None)
```

**Testing**
Following unit test is created to test the applied changes and added in the location as suggested in the guidelines.
adk-python\tests\unittests\models\test_base_llm.py

```
import pytest
from google.genai import types
from google.adk.models.llm_request import LlmRequest
from google.adk.models.lite_llm import _get_completion_inputs

@pytest.mark.parametrize("content_kwargs", [
    # Case 1: Explicit role provided
    {"role": "user", "parts": [types.Part(text="This is an input text from user.")]},
    # Case 2: Role omitted, should still be treated as 'user'
    {"parts": [types.Part(text="This is an input text from user.")]}
])
def test_user_content_role_defaults_to_user(content_kwargs):
    """
    Verifies that user-provided messages are always passed to the LLM as 'user' role,
    regardless of whether the role is explicitly set in types.Content.

    The helper `_get_completion_inputs` should give normalize messages so that
    explicit 'user' and implicit (missing role) are equivalent.
    """
    llm_request = LlmRequest(
        contents=[types.Content(**content_kwargs)],
        config=types.GenerateContentConfig()
    )

    messages, _, _, _ = _get_completion_inputs(llm_request)

    assert all(
        msg.get("role") == "user" for msg in messages
    ), f"Expected role 'user' but got {messages}"
    assert any(
        "This is an input text from user." == (msg.get("content") or "")
        for msg in messages
    ), f"Expected the user text to be preserved, but got {messages}"
```

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2458 from TanejaAnkisetty:bug/agent-user-content 381b01418d249b9e6bd91ebb518ff25339a8e47b
PiperOrigin-RevId: 809281620
2025-09-23 15:34:21 -07:00
Google Team Member 632bf8b0bc fix: Filter out thought parts when saving agent output to state
PiperOrigin-RevId: 809270320
2025-09-19 18:58:59 -07:00
Wei Sun (Jack) 6e834d3fac feat(conformance): Skips recording for inner runner of AgentTool in conformance tests
PiperOrigin-RevId: 809252704
2025-09-19 17:36:18 -07:00
Xiang (Sean) Zhou 9be9cc2fee feat: Support static instructions
Static instructions:
Always added to system instructions for context caching

Dynamic instructions:
Added to system instructions when no static instruction exists (for backward compatibility), OR inserted before last batch of continuous user content when static instructions exist

PiperOrigin-RevId: 809170679
2025-09-19 13:46:36 -07:00
Xiang (Sean) Zhou f4e1fd962e chore: Add sample agent for content cache and basic profiling
PiperOrigin-RevId: 809166922
2025-09-19 13:37:57 -07:00
Xiang (Sean) Zhou c66245a3b8 feat: support context caching
1. add a context cache config in app level which will apply to all agents in the app
2. pass on cache config through invocation context to llm_reqeust
3. store cache metadata in llm_response
4. lookup old cache metadata from latest event for reusing old cache
5. create new cache if old cache cannot be reused

PiperOrigin-RevId: 809158578
2025-09-19 13:17:02 -07:00
Xinran (Sherry) Tang 13a95c463d feat: Add get_events util function in invocation_context
PiperOrigin-RevId: 809111315
2025-09-19 11:21:35 -07:00
Kacper Jawoszek f157b2ee4c feat(otel): support standard OTel env variables for exporter endpoints
ADK web server will automatically setup OTel providers with exporters if any of the .*_ENDPOINT variables from https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/ is set.

PiperOrigin-RevId: 809079453
2025-09-19 09:59:58 -07:00
Bastien Jacot-Guillarmod ccd0e12b42 chore: Internal change
PiperOrigin-RevId: 809077633
2025-09-19 09:55:17 -07:00
Kacper Jawoszek 3b80337faf feat(otel): temporarily disable Cloud Monitoring integration in --otel_to_cloud
Currently there is chance for Cloud Monitoring-related errors in logs during shutdown. Let's disable metrics part until it is fixed.

PiperOrigin-RevId: 808930635
2025-09-19 01:28:15 -07:00
Xuan Yang d4eaa06041 chore: update ADK release analyzer agent to use the compare link instead of commit link
PiperOrigin-RevId: 808900352
2025-09-18 23:44:12 -07:00
Xuan Yang 4d39563ea4 chore: add yaml files to the ADK Vertex AI Search datastore
PiperOrigin-RevId: 808895175
2025-09-18 23:27:34 -07:00
Wei Sun (Jack) 006a406f5b chore: Allow outputting non-acsii without escape and excludes fields in the dumped yaml files in the yaml_utils.py
Also excludes `_adk_recordings_config` for `adk conformance create` command.

PiperOrigin-RevId: 808865049
2025-09-18 21:24:14 -07:00
Wei Sun (Jack) f39df4155e feat(conformance): Supports content and state_delta in TestCase.user_messages and initial_state for session creation
PiperOrigin-RevId: 808827170
2025-09-18 18:55:38 -07:00
Hangfei Lin 1a91bb2a59 chore: Update comments in Compaction to clarify timestamp-based ranges
The docstrings for `compaction_range` and `compacted_content` are updated to reflect that compaction is based on timestamp ranges rather than sequence IDs, and to use consistent terminology ("compacted" instead of "summarized").

PiperOrigin-RevId: 808770610
2025-09-18 15:51:40 -07:00
Wei Sun (Jack) 9c2b7091ee refactor(comformance): Improves field comparison logic in replay plugin with nested exclude dict from pydantic v2
Also use `ReplayConfigError` to replace `ValueError`s

PiperOrigin-RevId: 808750606
2025-09-18 15:01:19 -07:00
Nikhil Purwant 21c26f92d4 chore: Added ADK Authentication End2End Samples
Merge https://github.com/google/adk-python/pull/2960

1. All in one authentication sample (has an IDP, Agent and the application) under `contributing/samples/authn-adk-all-in-one/`
2. Documented for all the steps.
3. OAuth 2.0 Authorization Code Grant type used by the agent.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2960 from nikhilpurwant:main dfcc821602d265c4ae7cc42eb1f5739beaad6f87
PiperOrigin-RevId: 808672120
2025-09-18 11:44:21 -07:00
guillaume blaquiere 25958242db feat: add endpoint to generate memory from session
Merge https://github.com/google/adk-python/pull/2900

In relation with #2416

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2900 from guillaumeblaquiere:add-session-to-memory 0507de43021c62f9223167dca8f53b536227ad04
PiperOrigin-RevId: 808658162
2025-09-18 11:13:21 -07:00
Kel Markert 6b49391546 feat: Add Google Maps Grounding Tool to ADK
This add `GoogleMapsGroundingTool`, a built-in tool for Gemini 2 models to ground query results with Google Maps. This tool operates internally within the model and is only available when using the VertexAI Gemini API.

PiperOrigin-RevId: 808650501
2025-09-18 10:54:27 -07:00
Xuan Yang 8a92fd18b6 fix: ignore empty function chunk in LiteLlm streaming response
Fixes https://github.com/google/adk-python/issues/1532

PiperOrigin-RevId: 808636127
2025-09-18 10:18:53 -07:00
Hangfei Lin c37bd2742c feat: Introduce LLM context compaction interface
Provide a more efficient way to compact LLM context for better agentic performance.

* `app`: the top level abstraction for an ADK application. It contains an root agent, and plugins.
* `content_strategy`: the abstraction for selecting the contents for LLM request.
* `compaction_strategy`: the abstraction for compacting the events.
* Added `sequence_id` and `summary_range` in event class.

PiperOrigin-RevId: 808634224
2025-09-18 10:14:12 -07:00
Wei Sun (Jack) e86647d446 feat(conformance): Implements adk conformance test cli with replay mode
PiperOrigin-RevId: 808633566
2025-09-18 10:12:43 -07:00
Afonso Menegola c9ea80af28 fix: Prevent escaping of Latin characters in LLM response
Merge https://github.com/google/adk-python/pull/2937

**Closes #2936**

This Pull Request addresses the issue where `LlmAgent` outputs, when configured with `output_schema` and `tools`, were presenting escaped Latin characters (e.g., `\xf3` for `Ăł`) in the final response. This behavior occurred because `json.dumps` was being called with `ensure_ascii=True` (its default), which is not ideal for human-readable output, especially when dealing with non-ASCII characters common in many languages like Portuguese.

**Changes Proposed:**

* Modified the `_OutputSchemaRequestProcessor` in `src/google/adk/flows/llm_flows/_output_schema_processor.py` to explicitly set `ensure_ascii=False` when calling `json.dumps` for the `set_model_response` tool's output.

**Impact:**

This change ensures that all non-ASCII characters in the structured model response are preserved in their natural form, improving the readability and user experience of agent outputs, particularly for users interacting in languages with accented characters or other special symbols.

**Testing:**

The fix was verified locally by running an `LlmAgent` with an `output_schema` and confirming that responses containing Latin characters (e.g., "ação", "caminhão", "ícone") are now correctly displayed without escaping.

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/2937 from amenegola:fix/issue-2936-escape-chars 6cac00f97aa4cd8d8ccaa97ec5fffc74f57995dc
PiperOrigin-RevId: 808622892
2025-09-18 09:48:03 -07:00
Xiang (Sean) Zhou 86ee6e3fa3 fix: Close runners after running eval
this fixes https://github.com/google/adk-python/issues/2196

PiperOrigin-RevId: 808618368
2025-09-18 09:36:56 -07:00
Wei Sun (Jack) bf4ff31009 feat(conformance): add CLI (adk conformance create) for generating conformance tests from spec.yaml file
- Add conformance command group with create subcommand
- Implement category/name/spec.yaml with generated-*.yaml files
- Support executing agents with queries and recording sessions
- Create test cases with recorded llm interactions and tool calls/results

Expected folder structure:

```

conformance_repo/
├── agents/                            # Agent definitions - contains all config-based agents shared by test cases.
│   ├── single_basic/
│   ├── multi_basic/
│   └── single_tool_builtin/
│
└── tests/                             # Test cases
    ├── core/                          # Test category
    │   ├── desc_001/                  # Individual test case
    │   │   ├── spec.yaml             # Human-written specification
    │   │   ├── generated-session.yaml
    │   │   ├── generated-recordings.yaml
    │   │   └── ...                   # Potential future generated files
    │   ├── f_001/
    │   │   ├── spec.yaml
    │   │   ├── generated-session.yaml
    │   │   ├── generated-recordings.yaml
    │   │   └── ...

```

Help text:

```
-> % adk conformance create --help
Usage: adk conformance create [OPTIONS] [PATHS]...

  Generate ADK conformance test YAML files from TestCaseInput specifications.

  NOTE: this is work in progress.

  This command reads TestCaseInput specifications from input.yaml files, executes the specified test cases against agents, and generates conformance test files with recorded agent interactions as
  test.yaml files.

  Expected directory structure: category/name/input.yaml (TestCaseInput) -> category/name/test.yaml (TestCase)

  PATHS: One or more directories containing test case specifications. If no paths are provided, defaults to 'tests/' directory.

  Examples:

  Use default directory: adk conformance create

  Custom directories: adk conformance create tests/core tests/tools

Options:
  --help  Show this message and exit.
```
PiperOrigin-RevId: 808609547
2025-09-18 09:14:05 -07:00
Hangfei Lin 4cb07ba05e chore: Update plugins in hello_world_app
Corrected `CountInvocationPlugin` to be a class reference and added `ContextFilterPlugin` to limit the number of tool invocations kept in the context to 3.

PiperOrigin-RevId: 808591608
2025-09-18 08:26:42 -07:00
Kacper Jawoszek cee365a13d feat(otel): Add GenAI Instrumentation if --otel_to_cloud is enabled
PiperOrigin-RevId: 808460137
2025-09-18 01:33:13 -07:00
Wei Sun (Jack) 712da1bd36 feat(conformance): Integrates RecordingsPlugin into AdkWebServer to record Llm interactions and tool calls
When start the server with `--extra_plugins=google.adk.cli.plugins.recordings_plugin.RecordingsPlugin`, it will trigger recording with expected state in session.

PiperOrigin-RevId: 808432022
2025-09-18 00:05:06 -07:00
Xiang (Sean) Zhou 99405d6a8a chore: Fix the starting folder for finding ADK source
PiperOrigin-RevId: 808371099
2025-09-17 20:20:58 -07:00
Hangfei Lin a06bf278cb feat: Adding the ContextFilterPlugin
This commit introduces a new ContextFilterPlugin which allows for filtering the LlmRequest contents before they are sent to the LLM. This helps in managing and potentially reducing the size of the LLM context.

The plugin provides two primary filtering mechanisms:

num_invocations_to_keep: Keeps only the specified number of the most recent user-model invocations. An invocation is defined as one or more user messages followed by a model response.
custom_filter: Allows for a user-defined callable to be applied to the contents for more flexible filtering.
Unit tests have been added to cover the different filtering scenarios, including:

Filtering by the last N invocations.
Filtering using a custom function.
Combining both filtering methods.
Handling cases with multiple user turns in a single invocation.
Ensuring no filtering occurs when options are not provided.
Gracefully handling exceptions from custom filter functions."

For example, when num_of_innovacations=2:
-----------------------------------------------------------
Contents:
{"parts":[{"text":"9"}],"role":"user"}
{"parts":[{"text":"I am sorry, I cannot fulfill this request. I need more information on what you would like me to do. I can roll a die or check prime numbers.\n"}],"role":"model"}
{"parts":[{"text":"1"}],"role":"user"}
{"parts":[{"text":"I am sorry, I cannot fulfill this request. I need more information on what you would like me to do. I can roll a die or check prime numbers.\n"}],"role":"model"}
{"parts":[{"text":"10"}],"role":"user"}
-----------------------------------------------------------
PiperOrigin-RevId: 808355316
2025-09-17 19:28:56 -07:00
Google Team Member 10cf377494 feat: Make the bigquery sample agent run with ADC out-of-the-box
Right now the bigquery sample agent is configured to run with OAuth, which requires some set up. This change makes it more readily usable, both locally and in AgentEngine, as Application Default Credentials (ADC) is easier to set up, and often local and AgentEngine environment already have it set up.

PiperOrigin-RevId: 808315879
2025-09-17 16:52:49 -07:00
Wei Sun (Jack) 3bd2f29f3a feat(conformance): Adds a replay plugin to replay the previously recorded llm/tool recordings for conformance tests
PiperOrigin-RevId: 807979314
2025-09-16 21:54:25 -07:00
Xiang (Sean) Zhou 14f118899d chore: Add example agent to get log probabilitis
see https://github.com/google/adk-python/issues/2764

PiperOrigin-RevId: 807972596
2025-09-16 21:23:21 -07:00
Wei Sun (Jack) c0554e4b13 feat(conformance): add an ADK plugin to record Llm request/response and tool call/result to recordings.yaml file
Also moves the `Recordings` pydantic models into this plugins/ package.

Key features:
- Records LLM requests/responses and tool calls/results to YAML files in `generated-recordings.yaml`.
- Use session state to determine where to read and output recordings.

PiperOrigin-RevId: 807969100
2025-09-16 21:10:09 -07:00
Google Team Member 6bd33e1be3 fix: Retain the consumers and transport registry when recreating the ClientFactory in remote_a2a_agent.py
PiperOrigin-RevId: 807762203
2025-09-16 10:58:46 -07:00
Xiang (Sean) Zhou f7bd3c111c feat: Expose log probs of candidates in LlmResponse
fixes https://github.com/google/adk-python/issues/2764

PiperOrigin-RevId: 807516910
2025-09-15 21:36:15 -07:00
Wei Sun (Jack) 1ce043a278 chore: Fixes BasePlugin#after_run_callback return type hint
`Optional[None]` is `Union[None, None]`, which is essentially None.

PiperOrigin-RevId: 807515970
2025-09-15 21:33:05 -07:00
Xuan Yang bd21847295 chore: add a step to load adk-bot SSH Private Key for the release analysis workflow
PiperOrigin-RevId: 807479079
2025-09-15 19:20:36 -07:00
Kacper Jawoszek 1ae0b82f56 feat(otel): add --otel_to_cloud experimental support
Cloud Trace, Cloud Monitoring and Cloud Logging integrations are set up via OTel if otel_to_cloud CLI param/fast_api arg is provided.

This is similar to current Cloud Trace integration via trace_to_cloud, just extended to Monitoring and Logging as well.

PiperOrigin-RevId: 807385680
2025-09-15 14:32:22 -07:00
Xiang (Sean) Zhou d6d4b144e9 chore: Update instructions not to ask for root folder if user doesn't intent to create or implement an agent
PiperOrigin-RevId: 807372074
2025-09-15 13:59:08 -07:00
Xinran (Sherry) Tang 4dbec15d26 test: Add unittest suites for testing HITL confirmation flow on runner level
PiperOrigin-RevId: 807327997
2025-09-15 12:00:49 -07:00
Wei Sun (Jack) 402f3626b3 feat(conformance): Replaces invocation_id with user_message_index in the Recording
`invocation_id` is per-request, so we just need user_message_index to help locate where to start replay.

PiperOrigin-RevId: 807300016
2025-09-15 10:49:13 -07:00
Google Team Member 6158075a65 fix: introduces a raw_mcp_tool method in McpTool to provide direct access to the underlying MCP tool
PiperOrigin-RevId: 807299777
2025-09-15 10:48:13 -07:00
Google Team Member b1312680f4 feat(otel): add --otel_to_cloud experimental support
Cloud Trace, Cloud Monitoring and Cloud Logging integrations are set up via OTel if otel_to_cloud CLI param/fast_api arg is provided.

This is similar to current Cloud Trace integration via trace_to_cloud, just extended to Monitoring and Logging as well.

PiperOrigin-RevId: 807285744
2025-09-15 10:11:12 -07:00
Google Team Member 103e88e95f test: Add evaluation for BigQuery tools
We should treat this as the first step towards building a robust eval story for BQ tools.

PiperOrigin-RevId: 807247053
2025-09-15 08:16:33 -07:00
Kacper Jawoszek 7870480c63 feat(otel): add --otel_to_cloud experimental support
Cloud Trace, Cloud Monitoring and Cloud Logging integrations are set up via OTel if otel_to_cloud CLI param/fast_api arg is provided.

This is similar to current Cloud Trace integration via trace_to_cloud, just extended to Monitoring and Logging as well.

PiperOrigin-RevId: 807230668
2025-09-15 07:22:19 -07:00
Bastien Jacot-Guillarmod b9735b2193 docs: Correct the documentation of after_agent_callback
The `after_agent_callback` in plugin works similarly as the `after_agent_callback` in `base_agent.py`, e.g. it only append new content, but cannot modify the previous content.

PiperOrigin-RevId: 807162139
2025-09-15 03:06:57 -07:00
Wei Sun (Jack) 8ec83d6c18 feat(conformance): Add data definitions used to create adn run conformance tests
PiperOrigin-RevId: 807100057
2025-09-14 23:30:44 -07:00
165 changed files with 19148 additions and 694 deletions
@@ -23,6 +23,11 @@ jobs:
with:
python-version: '3.11'
- name: Load adk-bot SSH Private Key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.ADK_BOT_SSH_PRIVATE_KEY }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
@@ -16,7 +16,6 @@
from pathlib import Path
from typing import Callable
from typing import Literal
from typing import Optional
from typing import Union
@@ -46,46 +45,22 @@ class AgentBuilderAssistant:
@staticmethod
def create_agent(
model: Union[str, BaseLlm] = "gemini-2.5-flash",
schema_mode: Literal["embedded", "query"] = "embedded",
working_directory: Optional[str] = None,
) -> LlmAgent:
"""Create Agent Builder Assistant with configurable ADK AgentConfig schema approach.
"""Create Agent Builder Assistant with embedded ADK AgentConfig schema.
Args:
model: Model to use for the assistant (default: gemini-2.5-flash)
schema_mode: ADK AgentConfig schema handling approach: - "embedded": Embed
full ADK AgentConfig schema in instructions (default) - "query": Use
query_schema tool for dynamic ADK AgentConfig schema access
working_directory: Working directory for path resolution (default: current
working directory)
Returns:
Configured LlmAgent with specified ADK AgentConfig schema mode
Configured LlmAgent with embedded ADK AgentConfig schema
"""
# ADK AGENTCONFIG SCHEMA MODE SELECTION: Choose between two approaches for ADK AgentConfig schema access
#
# Why two modes?
# 1. Token efficiency: Embedded mode front-loads ADK AgentConfig schema in context vs
# Query mode which fetches ADK AgentConfig schema details on-demand
# 2. Performance: Embedded mode provides immediate access vs Query mode
# which requires tool calls for each ADK AgentConfig schema query
# 3. Use case fit: Embedded for comprehensive ADK AgentConfig schema work, Explorer for
# targeted queries and token-conscious applications
#
# Mode comparison:
# Embedded: Fast, comprehensive, higher token usage
# Query: Dynamic, selective, lower initial token usage
if schema_mode == "embedded":
# Load full ADK AgentConfig schema directly into instruction context
instruction = AgentBuilderAssistant._load_instruction_with_schema(
model, working_directory
)
else: # schema_mode == "query"
# Use schema query tool for dynamic ADK AgentConfig schema access
instruction = AgentBuilderAssistant._load_instruction_with_query(
model, working_directory
)
# Load full ADK AgentConfig schema directly into instruction context
instruction = AgentBuilderAssistant._load_instruction_with_schema(
model, working_directory
)
# TOOL ARCHITECTURE: Hybrid approach using both AgentTools and FunctionTools
#
@@ -124,17 +99,6 @@ class AgentBuilderAssistant:
FunctionTool(search_adk_source), # Search ADK source with regex
]
# CONDITIONAL TOOL LOADING: Add ADK AgentConfig schema query tool only in query mode
#
# Why conditional?
# - Embedded mode already has ADK AgentConfig schema in context, doesn't need explorer
# - Query mode needs dynamic ADK AgentConfig schema access via tool calls
# - Keeps tool list lean and relevant to the chosen ADK AgentConfig schema approach
if schema_mode == "explorer":
from .tools.query_schema import query_schema
custom_tools.append(FunctionTool(query_schema))
# Combine all tools
all_tools = agent_tools + custom_tools
@@ -211,34 +175,6 @@ class AgentBuilderAssistant:
return instruction_provider
@staticmethod
def _load_instruction_with_query(
model: Union[str, BaseLlm],
working_directory: Optional[str] = None,
) -> Callable[[ReadonlyContext], str]:
"""Load instruction template for ADK AgentConfig schema query mode."""
query_template = (
AgentBuilderAssistant._load_query_schema_instruction_template()
)
# Get model string for template replacement
model_str = (
str(model)
if isinstance(model, str)
else getattr(model, "model_name", str(model))
)
# Fill the instruction template with default model
instruction_text = query_template.format(default_model=model_str)
# Return a function that accepts ReadonlyContext and returns the instruction
def instruction_provider(context: ReadonlyContext) -> str:
return AgentBuilderAssistant._compile_instruction_with_context(
instruction_text, context, working_directory
)
return instruction_provider
@staticmethod
def _load_embedded_schema_instruction_template() -> str:
"""Load instruction template for embedded ADK AgentConfig schema mode."""
@@ -252,19 +188,6 @@ class AgentBuilderAssistant:
with open(template_path, "r", encoding="utf-8") as f:
return f.read()
@staticmethod
def _load_query_schema_instruction_template() -> str:
"""Load instruction template for ADK AgentConfig schema query mode."""
template_path = Path(__file__).parent / "instruction_query.template"
if not template_path.exists():
raise FileNotFoundError(
f"Query instruction template not found at {template_path}"
)
with open(template_path, "r", encoding="utf-8") as f:
return f.read()
@staticmethod
def _compile_instruction_with_context(
instruction_text: str,
@@ -6,6 +6,12 @@ You are an intelligent Agent Builder Assistant specialized in creating and confi
Help users design, build, and configure sophisticated multi-agent systems for the ADK framework. You guide users through the agent creation process by asking clarifying questions, suggesting optimal architectures, and generating properly formatted YAML configuration files that comply with the ADK AgentConfig schema.
## CRITICAL BEHAVIOR RULE
**NEVER assume users want to create agents unless they explicitly ask to CREATE, BUILD, GENERATE, IMPLEMENT, or UPDATE something.**
When users ask informational questions like "find me examples", "show me samples", "how do I", etc., they want INFORMATION ONLY. Provide the information and stop. Do not offer to create anything or ask for root directories.
## Core Capabilities
1. **Agent Architecture Design**: Analyze requirements and suggest appropriate agent types (LlmAgent, SequentialAgent, ParallelAgent, LoopAgent)
@@ -26,7 +32,27 @@ Always reference this schema when creating configurations to ensure compliance.
## Workflow Guidelines
### 1. Discovery Phase
- **ROOT DIRECTORY ESTABLISHMENT**:
- **DETERMINE USER INTENT FIRST**:
* **INFORMATIONAL QUESTIONS** (Answer directly WITHOUT asking for root directory):
- "Could you find me examples of..." / "Find me samples of..."
- "Show me how to..." / "How do I..."
- "What is..." / "What are..." / "Explain..."
- "Can you show me..." / "Do you have examples of..."
- "I'm looking for information about..." / "I need to understand..."
- Questions about ADK capabilities, concepts, or existing implementations
- **CRITICAL**: For informational questions, provide the requested information and STOP. Do NOT offer to create, build, or generate anything unless explicitly asked.
* **CREATION/BUILDING INTENT** (Only then ask for root directory):
- "Create a new agent..." / "Build me an agent..."
- "Generate an agent..." / "Implement an agent..."
- "Update my agent..." / "Modify my agent..." / "Change my agent..."
- "I want to create..." / "Help me build..." / "Help me update..."
- "Set up a project..." / "Make me an agent..."
**EXAMPLE OF CORRECT BEHAVIOR:**
- User: "Could you find me a sample agent that can list my calendar events?"
- âś… CORRECT: Search for examples, show the samples found, explain how they work, and STOP.
- ❌ WRONG: "Before I proceed with creating an agent..." or asking for root directory.
- **ROOT DIRECTORY ESTABLISHMENT** (Only for Creation/Building):
* **FIRST**: Check SESSION CONTEXT section below for "Established Root Directory"
* **IF ESTABLISHED**: Use the existing session root directory - DO NOT ask again
* **IF NOT ESTABLISHED**: Ask user for root directory to establish working context
@@ -1,297 +0,0 @@
# Agent Builder Assistant - Query Schema Mode
You are an intelligent Agent Builder Assistant specialized in creating and configuring ADK (Agent Development Kit) multi-agent systems using YAML configuration files.
## Your Purpose
Help users design, build, and configure sophisticated multi-agent systems for the ADK framework. You guide users through the agent creation process by asking clarifying questions, suggesting optimal architectures, and generating properly formatted YAML configuration files that comply with the ADK AgentConfig schema.
## Core Capabilities
1. **Agent Architecture Design**: Analyze requirements and suggest appropriate agent types (LlmAgent, SequentialAgent, ParallelAgent, LoopAgent)
2. **YAML Configuration Generation**: Create proper ADK agent configuration files with correct ADK AgentConfig schema compliance
3. **Tool Integration**: Help configure and integrate various tool types (Function tools, Google API tools, MCP tools, etc.)
4. **Python File Management**: Create, update, and delete Python files for custom tools and callbacks per user request
5. **Project Structure**: Guide proper ADK project organization and file placement
6. **ADK AgentConfig Schema Querying**: Use the query_schema to dynamically query ADK AgentConfig schema for accurate field definitions
7. **ADK Knowledge & Q&A**: Answer questions about ADK concepts, APIs, usage patterns, troubleshooting, and best practices using comprehensive research capabilities
## ADK AgentConfig Schema Information
Instead of embedding the full ADK AgentConfig schema, you have access to the `query_schema` that allows you to:
- Query ADK AgentConfig schema overview: Use query_type="overview" to get high-level structure
- Explore ADK AgentConfig schema components: Use query_type="component" with component name (e.g., "tools", "model")
- Get ADK AgentConfig schema field details: Use query_type="field" with field_path (e.g., "tools.function_tool.function_path")
- List all ADK AgentConfig schema properties: Use query_type="properties" to get comprehensive property list
Always use the query_schema tool when you need specific ADK AgentConfig schema information to ensure accuracy.
## Workflow Guidelines
### 1. Discovery Phase
- **ROOT DIRECTORY ESTABLISHMENT**:
* **FIRST**: Check SESSION CONTEXT section below for "Established Root Directory"
* **IF ESTABLISHED**: Use the existing session root directory - DO NOT ask again
* **IF NOT ESTABLISHED**: Ask user for root directory to establish working context
- **MODEL PREFERENCE**: Only ask for model preference when you determine that LlmAgent(s) will be needed
* **When to ask**: After analyzing requirements and deciding that LlmAgent is needed for the solution
* **DEFAULT**: Use "{default_model}" (your current model) if user doesn't specify
* **EXAMPLES**: "gemini-2.5-flash", "gemini-2.5-pro", etc.
* **RATIONALE**: Only LlmAgent requires model specification; workflow agents do not
- **CRITICAL PATH RESOLUTION**: If user provides a relative path (e.g., `./config_agents/roll_and_check`):
* **FIRST**: Call `resolve_root_directory` to get the correct absolute path
* **VERIFY**: The resolved path matches user's intended location
* **EXAMPLE**: `./config_agents/roll_and_check` should resolve to `/Users/user/Projects/adk-python/config_agents/roll_and_check`, NOT `/config_agents/roll_and_check`
- Understand the user's goals and requirements through targeted questions
- Explore existing project structure using the RESOLVED ABSOLUTE PATH
- Identify integration needs (APIs, databases, external services)
### 2. Design Phase
- Present a clear architecture design BEFORE implementation
- Explain your reasoning and ask for user confirmation
- Suggest appropriate agent types and tool combinations
- Consider scalability and maintainability
### 3. Implementation Phase
**MANDATORY CONFIRMATION BEFORE ANY WRITES:**
- **NEVER write any file without explicit user confirmation**
- **Always present proposed changes first** and ask "Should I proceed with these changes?"
- **For modifications**: Show exactly what will be changed and ask for approval
- **For new files**: Show the complete content and ask for approval
- **For existing file modifications**: Ask "Should I create a backup before modifying this file?"
- **Use backup_existing parameter**: Set to True only if user explicitly requests backup
**IMPLEMENTATION ORDER (CRITICAL - ONLY AFTER USER CONFIRMS DESIGN):**
**STEP 1: YAML CONFIGURATION FILES FIRST**
1. Generate all YAML configuration files
2. Present complete YAML content to user for confirmation
3. Ask: "Should I create these YAML configuration files?"
4. Only proceed after user confirmation
**STEP 2: PYTHON FILES SECOND**
1. Generate Python tool/callback files
2. Present complete Python content to user for confirmation
3. Ask: "Should I create these Python files?"
4. Only proceed after user confirmation
1. **Present all proposed changes** - Show exact file contents and modifications
2. **Get explicit user approval** - Wait for "yes" or "proceed" before any writes
3. **Execute approved changes** - Only write files after user confirms
* ⚠️ **YAML files**: Use `write_config_files` (root_agent.yaml, etc.)
* ⚠️ **Python files**: Use `write_files` (tools/*.py, etc.)
4. **Clean up unused files** - Use cleanup_unused_files and delete_files to remove obsolete tool files
**YAML Configuration Requirements:**
- Main agent file MUST be named `root_agent.yaml`
- **Sub-agent placement**: Place ALL sub-agent YAML files in the root folder, NOT in `sub_agents/` subfolder
- Tool paths use format: `project_name.tools.module.function_name` (must start with project folder name, no `.py` extension, all dots)
* **Example**: For project at `config_agents/roll_and_check` with tool in `tools/is_prime.py`, use: `roll_and_check.tools.is_prime.is_prime`
* **Pattern**: `{{{{project_folder_name}}}}.tools.{{{{module_name}}}}.{{{{function_name}}}}`
* **CRITICAL**: Use only the final component of the root folder path as project_folder_name (e.g., for `./config_based/roll_and_check`, use `roll_and_check` not `config_based.roll_and_check`)
- No function declarations in YAML (handled automatically by ADK)
**TOOL IMPLEMENTATION STRATEGY:**
- **For simple/obvious tools**: Implement them directly with actual working code
* Example: dice rolling, prime checking, basic math, file operations
* Don't ask users to "fill in TODO comments" for obvious implementations
- **For complex/business-specific tools**: Generate proper function signatures with TODO comments
* Example: API integrations requiring API keys, complex business logic
- **Always generate correct function signatures**: If user wants `roll_dice` and `is_prime`, generate those exact functions, not generic `tool_name`
**CRITICAL: Tool Usage Patterns - MANDATORY FILE TYPE SEPARATION**
⚠️ **YAML FILES (.yaml, .yml) - MUST USE CONFIG TOOLS:**
- **ALWAYS use `write_config_files`** for writing YAML configuration files (root_agent.yaml, etc.)
- **ALWAYS use `read_config_files`** for reading YAML configuration files
- **NEVER use `write_files` for YAML files** - it lacks validation and schema compliance
⚠️ **PYTHON/OTHER FILES (.py, .txt, .md) - USE GENERAL FILE TOOLS:**
- **Use `write_files`** for Python tools, scripts, documentation, etc.
- **Use `read_files`** for non-YAML content
⚠️ **WHY THIS SEPARATION MATTERS:**
- `write_config_files` validates YAML syntax and ADK AgentConfig schema compliance
- `write_files` is raw file writing without validation
- Using wrong tool can create invalid configurations
- **For ADK code questions**: Use `search_adk_source` then `read_files` for complete context
- **File deletion**: Use `delete_files` for multiple file deletion with backup options
**TOOL GENERATION RULES:**
- **Match user requirements exactly**: Generate the specific functions requested
- **Use proper parameter types**: Don't use generic `parameter: str` when specific types are needed
- **Implement when possible**: Write actual working code for simple, well-defined functions
- **ONE TOOL PER FILE POLICY**: Always create separate files for individual tools
* **Example**: Create `roll_dice.py` and `is_prime.py` instead of `dice_tools.py`
* **Benefit**: Enables easy cleanup when tools are no longer needed
* **Exception**: Only use multi-tool files for legitimate toolsets with shared logic
### 4. Validation Phase
- Review generated configurations for schema compliance
- Test basic functionality when possible
- Provide clear next steps for the user
## Available Tools
You have access to comprehensive tools for:
- **Configuration Management**: Read/write multiple YAML configs with validation and schema compliance
- **File Management**: Read/write multiple files (Python tools, scripts, documentation) with full content handling
- **Project Exploration**: Analyze directory structures and suggest file locations
- **Schema Exploration**: Query AgentConfig schema dynamically for accurate field information
- **ADK Source Search**: Search ADK source code with regex patterns for precise code lookups
- **ADK Knowledge**: Research ADK concepts using local source search and web-based tools
- **Research**: Search GitHub examples and fetch relevant code samples
- **Working Directory**: Resolve paths and maintain context
### When to Use Research Tools
**ALWAYS use research tools when:**
1. **User asks ADK questions**: Any questions about ADK concepts, APIs, usage patterns, or troubleshooting
2. **Unfamiliar ADK features**: When user requests features you're not certain about
3. **Agent type clarification**: When unsure about agent types, their capabilities, or configuration
4. **Best practices**: When user asks for examples or best practices
5. **Error troubleshooting**: When helping debug ADK-related issues
6. **Agent building uncertainty**: When unsure how to create agents or what's the best practice
7. **Architecture decisions**: When evaluating different approaches or patterns for agent design
**Research Tool Usage Patterns:**
**For ADK Code Questions (NEW - Preferred Method):**
1. **search_adk_source** - Find exact code patterns with regex
2. **read_files** - Get complete file context for detailed analysis
3. **query_schema** - Query AgentConfig schema for field definitions
**For External Examples and Documentation:**
- **google_search_agent**: Search and analyze web content (returns full page content, not just URLs)
* Search within key repositories: "site:github.com/google/adk-python ADK SequentialAgent examples"
* Search documentation: "site:github.com/google/adk-docs agent configuration patterns"
* General searches: "ADK workflow patterns", "ADK tool integration patterns"
* Returns complete page content as search results - no need for additional URL fetching
- **url_context_agent**: Fetch specific URLs only when:
* Specific URLs are mentioned in search results that need additional content
* User provides specific URLs in their query
* You need to fetch content from URLs found within google_search results
* NOT needed for general searches - google_search_agent already provides page content
**Research for Agent Building:**
- When user requests complex multi-agent systems: Search for similar patterns in samples
- When unsure about tool integration: Look for tool usage examples in contributing/samples
- When designing workflows: Find SequentialAgent, ParallelAgent, or LoopAgent examples
- When user needs specific integrations: Search for API, database, or service integration examples
## Code Generation Guidelines
### When Creating Python Tools or Callbacks:
1. **Always search for current examples first**: Use google_search_agent to find "ADK tool_context examples" or "ADK callback_context examples"
2. **Reference contributing/samples**: Use google_search_agent to find examples, or url_context_agent only if specific URLs are identified that need additional content
3. **Look for similar patterns**: Search for tools or callbacks that match your use case
4. **Use snake_case**: Function names should be snake_case (e.g., `check_prime`, `roll_dice`)
5. **Remove tool suffix**: Don't add "_tool" to function names
6. **Implement simple functions**: For obvious functions like `is_prime`, `roll_dice`, replace TODO with actual implementation
7. **Keep TODO for complex**: For complex business logic, leave TODO comments
8. **Follow current ADK patterns**: Always search for and reference the latest examples from contributing/samples
### Research and Examples:
- Use google_search_agent to find "ADK [use-case] examples" or "ADK [pattern] configuration" (returns full content)
- Use url_context_agent only when:
* Specific URLs are found in search results that need additional content
* User provides specific URLs to analyze
* You need to fetch specific examples from identified URLs:
* GitHub repositories: https://github.com/google/adk-samples/
* Contributing examples: https://github.com/google/adk-python/tree/main/contributing
* Documentation: https://github.com/google/adk-docs
- Adapt existing patterns to user requirements while maintaining compliance
## Important ADK Requirements
**File Naming & Structure:**
- Main configuration MUST be `root_agent.yaml` (not `agent.yaml`)
- Agent directories need `__init__.py` with `from . import agent`
- Python files in agent directory, YAML at root level
**Tool Configuration:**
- Function tools: `project_name.tools.module.function_name` format (all dots, must start with project folder name)
- No `.py` extension in tool paths
- No function declarations needed in YAML
- **Critical**: Tool paths must include the project folder name as the first component (final component of root folder path only)
**ADK Agent Types and Model Field Rules:**
- **LlmAgent**: REQUIRES `model` field - this agent directly uses LLM for responses
- **SequentialAgent**: NO `model` field - workflow agent that orchestrates other agents in sequence
- **ParallelAgent**: NO `model` field - workflow agent that runs multiple agents in parallel
- **LoopAgent**: NO `model` field - workflow agent that executes agents in a loop
- **CRITICAL**: Only LlmAgent accepts a model field. Workflow agents (Sequential/Parallel/Loop) do NOT have model fields
**ADK AgentConfig Schema Compliance:**
- Always use query_schema to verify ADK AgentConfig schema field requirements
- **MODEL FIELD RULES**:
* **LlmAgent**: `model` field is REQUIRED - Ask user for preference only when LlmAgent is needed, use "{default_model}" if not specified
* **Workflow Agents**: `model` field is FORBIDDEN - Remove model field entirely for Sequential/Parallel/Loop agents
- Optional fields: description, instruction, tools, sub_agents as defined in ADK AgentConfig schema
## Critical Path Handling Rules
**NEVER assume relative path context** - Always resolve paths first!
### For relative paths provided by users:
1. **ALWAYS call `resolve_root_directory`** to convert relative to absolute path
2. **Verify the resolved path** matches user's intended location
3. **Use the resolved absolute path** for all file operations
### Examples:
- **User input**: `./config_agents/roll_and_check`
- **WRONG approach**: Create files at `/config_agents/roll_and_check`
- **CORRECT approach**:
1. Call `resolve_root_directory("./config_agents/roll_and_check")`
2. Get resolved path: `/Users/user/Projects/adk-python/config_agents/roll_and_check`
3. Use the resolved absolute path for all operations
### When to use path resolution tools:
- **`resolve_root_directory`**: When user provides relative paths or you need to verify path context
- **`get_working_directory_info`**: When execution context seems incorrect or working directory is unclear
## Success Criteria
### Design Phase Success:
1. Root folder path confirmed and analyzed with explore_project
2. Clear understanding of user requirements through targeted questions
3. Well-researched architecture based on proven ADK patterns
4. Comprehensive design proposal with agent relationships, tool mappings, AND specific file paths
5. User approval of both architecture and file structure before any implementation
### Implementation Phase Success:
1. Files created at exact paths specified in approved design
2. No redundant suggest_file_path calls for pre-approved paths
3. Generated configurations pass schema validation (automatically checked)
4. Follow ADK naming and organizational conventions
5. Be immediately testable with `adk run [root_directory]` or via `adk web` interface
6. Include clear, actionable instructions for each agent
7. Use appropriate tools for intended functionality
## Key Reminder
**Your primary role is to be a collaborative architecture consultant that follows an efficient, user-centric workflow:**
1. **Always ask for root folder first** - Know where to create the project
2. **Design with specific paths** - Include exact file locations in proposals
3. **Provide high-level architecture overview** - When confirming design, always include:
* Overall system architecture and component relationships
* Agent types and their responsibilities
* Tool integration patterns and data flow
* File structure with clear explanations of each component's purpose
4. **Get complete approval** - Architecture, design, AND file structure confirmed together
5. **Implement efficiently** - Use approved paths directly without redundant tool calls
6. **Focus on collaboration** - Ensure user gets exactly what they need with clear understanding
**This workflow eliminates inefficiencies and ensures users get well-organized, predictable file structures in their chosen location.**
## Running Generated Agents
**Correct ADK Commands:**
- `adk run [root_directory]` - Run agent from root directory (e.g., `adk run config_agents/roll_and_check`)
- `adk web [parent_directory]` - Start web interface, then select agent from dropdown menu (e.g., `adk web config_agents`)
**Incorrect Commands to Avoid:**
- `adk run [root_directory]/root_agent.yaml` - Do NOT specify the YAML file directly
- `adk web` without parent directory - Must specify the parent folder containing the agent projects
- Always use the project directory for `adk run`, and parent directory for `adk web`
@@ -23,7 +23,7 @@ from typing import Dict
from typing import Optional
# Set up logger for ADK source utils
logger = logging.getLogger(__name__)
logger = logging.getLogger("google_adk." + __name__)
# Global cache for ADK AgentConfig schema to avoid repeated file reads
_schema_cache: Optional[Dict[str, Any]] = None
@@ -49,7 +49,7 @@ def find_adk_source_folder(start_path: Optional[str] = None) -> Optional[str]:
adk_path = find_adk_source_folder("/path/to/project")
"""
if start_path is None:
start_path = os.getcwd()
start_path = os.path.dirname(__file__)
current_path = Path(start_path).resolve()
@@ -72,8 +72,8 @@ def upload_directory_to_gcs(
# into hidden directories.
dirs[:] = [d for d in dirs if not d.startswith(".")]
# Keep only .md and .py files.
files = [f for f in files if f.endswith(".md") or f.endswith(".py")]
# Keep only .md, .py and .yaml files.
files = [f for f in files if f.endswith((".md", ".py", ".yaml"))]
for filename in files:
local_path = os.path.join(root, filename)
@@ -99,6 +99,19 @@ def upload_directory_to_gcs(
bucket.blob(gcs_path).upload_from_string(
html_content, content_type=content_type
)
elif filename.lower().endswith(".yaml"):
# Vertex AI search doesn't recognize yaml,
# convert it to text and use text/plain instead
content_type = "text/plain"
with open(local_path, "r", encoding="utf-8") as f:
yaml_content = f.read()
if not yaml_content:
print(" - Skipped empty file: " + local_path)
continue
gcs_path = gcs_path.removesuffix(".yaml") + ".txt"
bucket.blob(gcs_path).upload_from_string(
yaml_content, content_type=content_type
)
else: # Python files
bucket.blob(gcs_path).upload_from_filename(
local_path, content_type=content_type
@@ -115,6 +115,10 @@ def convert_gcs_to_https(gcs_uri: str) -> Optional[str]:
if relative_path.endswith(".html"):
relative_path = relative_path.removesuffix(".html") + ".md"
# Replace .txt with .yaml
if relative_path.endswith(".txt"):
relative_path = relative_path.removesuffix(".txt") + ".yaml"
# Convert the links for adk-docs
if prefix == "adk-docs" and relative_path.startswith("docs/"):
path_after_docs = relative_path[len("docs/") :]
@@ -100,13 +100,13 @@ root_agent = Agent(
Explanation of why this change is necessary.
**Reference**:
Reference to the code change (e.g. https://github.com/google/adk-python/commit/b3b70035c432670a5f0b5cdd1e9467f43b80495c).
Reference to the code file (e.g. src/google/adk/tools/spanner/metadata_tool.py).
```
- When referncing doc file, use the full relative path of the doc file in the ADK Docs repository (e.g. docs/sessions/memory.md).
9. Create or recommend to create a Github issue in the Github Repository {DOC_REPO} with the instructions using the `create_issue` tool.
- The title of the issue should be "Found docs updates needed from ADK python release <start_tag> to <end_tag>", where start_tag and end_tag are the release tags.
- The body of the issue should be the instructions about how to update the ADK docs.
- Include the compare link between the two ADK releases in the issue body, e.g. https://github.com/google/adk-python/compare/v1.14.0...v1.14.1.
- **{APPROVAL_INSTRUCTION}**
# 4. Guidelines & Rules
@@ -0,0 +1,152 @@
## ADK Authentication Demo (All in one - Agent, IDP and The app)
This folder contains everything you need to run the ADK's `auth-code`
grant type authentication demo completely locally
Here's the high level diagram.
![alt](doc_images/adk-auth-all-in-one.svg)
### Introduction
More often than not the agents use some kind of system identity
(especially for OpenAPI and MCP tools).
But obviously this is insecure in that multiple end users
are using the same identity with permissions to access ALL users' data on the
backend.
ADK provides various [authentication mechanisms](https://google.github.io/adk-docs/tools/authentication/) to solve this.
However to properly test it you need various components.
We provide everything that is needed so that you can test and run
ADK authentication demo locally.
This folder comes with -
1. An IDP
2. A hotel booking application backend
3. A hotel assistant ADK agent (accessing the application using OpenAPI Tools)
### Details
You can read about the Auth Code grant / flow type in detail [here](https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type). But for the purpose of this demo, following steps take place
1. The user asks the agent to find hotels in "New York".
2. Agent realizes (based on LLM response) that it needs to call a tool and that the tool needs authentication.
3. Agent redirects the user to the IDP's login page with callback / redirect URL back to ADK UI.
4. The user enters credentials (`john.doe` and `password123`) and accepts the consent.
5. The IDP sends the auth_code back to the redirect URL (from 3).
6. ADK then exchanges this auth_code for an access token.
7. ADK does the API call to get details on hotels and hands over that response to LLM, LLM formats the response.
8. ADK sends a response back to the User.
### Setting up and running
1. Clone this repository
2. Carry out following steps and create and activate the environment
```bash
# Go to the cloned directory
cd adk-python
# Navigate to the all in one authentication sample
cd contributing/samples/authn-adk-all-in-one/
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
```
3. Configure and Start the IDP. Our IDP needs a private key to sign the tokens and a JWKS with public key component to verify them. Steps are provided for that (please check the screenshots below)
🪧 **NOTE:**
It is recommended that you execute the key pair creation and public
key extraction commands (1-3 and 5 below) on Google cloud shell.
```bash
cd idp
# Create .env file by copying the existing one.
cp sample.env .env
cp sample.jwks.json jwks.json
# Carry out following steps
# 1. Generate a key pair, When asked about passphrase please press enter (empty passphrase)
ssh-keygen -t rsa -b 2048 -m PEM -f private_key.pem
# 2. Extract the public key
openssl rsa -in private_key.pem -pubout > pubkey.pub
# 3. Generate the jwks.json content using https://jwkset.com/generate and this public key (choose key algorithm RS256 and Key use Signature) (Please check the screenshot)
# 4. Update the jwks.json with the key jwks key created in 3 (please check the screenshot)
# 5. Update the env file with the private key
cat private_key.pem | tr -d "\n"
# 6. Carefully copy output of the command above into the .env file to update the value of PRIVATE_KEY
# 7. save jwks.json and .env
# Start the IDP
python app.py
```
<details>
<summary><b>Screenshots</b></summary>
Generating JWKS -
![alt](doc_images/jwksgen.png)
Updated `jwks.json` (notice the key is added in the existing array)
![alt](doc_images/jwks_updated.png)
</details>
4. In a separate shell - Start the backend API (Hotel Booking Application)
```bash
# Go to the cloned directory
cd adk-python
# Navigate to the all in one authentication sample
cd contributing/samples/authn-adk-all-in-one/
# Activate Env for this shell
. .venv/bin/activate
cd hotel_booker_app/
# Start the hotel booker application
python main.py
```
5. In a separate shell - Start the ADK agent
```bash
# Go to the cloned directory
cd adk-python
# Navigate to the all in one authentication sample
cd contributing/samples/authn-adk-all-in-one/
# Activate Env for this shell
. .venv/bin/activate
cd adk_agents/
cp sample.env .env
# ⚠️ Make sure to update the API KEY (GOOGLE_API_KEY) in .env file
# Run the agent
adk web
```
6. Access the agent on http://localhost:8000
🪧 **NOTE:**
After first time authentication,
it might take some time for the agent to respond,
subsequent responses are significantly faster.
### Conclusion
You can exercise the ADK Authentication
without any external components using this demo.
@@ -0,0 +1,15 @@
# 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 . import agent
@@ -0,0 +1,65 @@
# 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.
import logging
import os
from google.adk.tools.openapi_tool.auth.auth_helpers import openid_url_to_scheme_credential
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset
credential_dict = {
"client_id": os.environ.get("OAUTH_CLIENT_ID"),
"client_secret": os.environ.get("OAUTH_CLIENT_SECRET"),
}
auth_scheme, auth_credential = openid_url_to_scheme_credential(
openid_url="http://localhost:5000/.well-known/openid-configuration",
credential_dict=credential_dict,
scopes=[],
)
# Open API spec
file_path = "./agent_openapi_tools/openapi.yaml"
file_content = None
try:
with open(file_path, "r") as file:
file_content = file.read()
except FileNotFoundError:
# so that the execution does not continue when the file is not found.
raise FileNotFoundError(f"Error: The API Spec '{file_path}' was not found.")
# Example with a JSON string
openapi_spec_yaml = file_content # Your OpenAPI YAML string
openapi_toolset = OpenAPIToolset(
spec_str=openapi_spec_yaml,
spec_str_type="yaml",
auth_scheme=auth_scheme,
auth_credential=auth_credential,
)
from google.adk.agents import LlmAgent
root_agent = LlmAgent(
name="hotel_agent",
instruction=(
"Help user find and book hotels, fetch their bookings using the tools"
" provided."
),
description="Hotel Booking Agent",
model=os.environ.get("GOOGLE_MODEL"),
tools=[openapi_toolset], # Pass the toolset
# ... other agent config ...
)
@@ -0,0 +1,229 @@
openapi: 3.0.0
info:
title: Hotel Booker API
description: A simple API for managing hotel bookings, with a custom client credentials authentication flow.
version: 1.0.0
servers:
- url: http://127.0.0.1:8081
paths:
/hotels:
get:
summary: Get available hotels
description: Retrieves a list of available hotels, optionally filtered by location.
security:
- BearerAuth: []
parameters:
- in: query
name: location
schema:
type: string
description: The city to filter hotels by (e.g., 'New York').
responses:
'200':
description: Successfully retrieved hotels.
content:
application/json:
schema:
type: object
properties:
error:
type: boolean
example: false
data:
type: array
items:
$ref: '#/components/schemas/Hotel'
message:
type: string
example: "Successfully retrieved hotels."
'401':
description: Unauthorized. Invalid or expired token.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/book:
post:
summary: Book a room
description: Books a room in a specified hotel.
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BookingRequest'
responses:
'200':
description: Booking successful.
content:
application/json:
schema:
type: object
properties:
error:
type: boolean
example: false
data:
type: object
properties:
booking_id:
type: string
example: "HB-1"
message:
type: string
example: "Booking successful!"
'400':
description: Bad request. Missing information or invalid booking details.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized. Invalid or expired token.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/booking_details:
get:
summary: Get booking details
description: Retrieves details for a specific booking by ID or guest name.
security:
- BearerAuth: []
parameters:
- in: query
name: booking_id
schema:
type: string
description: The custom booking ID (e.g., 'HB-1').
- in: query
name: guest_name
schema:
type: string
description: The name of the guest to search for (partial and case-insensitive).
responses:
'200':
description: Booking details retrieved successfully.
content:
application/json:
schema:
type: object
properties:
error:
type: boolean
example: false
data:
type: object
properties:
custom_booking_id:
type: string
example: "HB-1"
hotel_name:
type: string
example: "Grand Hyatt"
hotel_location:
type: string
example: "New York"
guest_name:
type: string
example: "John Doe"
check_in_date:
type: string
example: "2025-10-01"
check_out_date:
type: string
example: "2025-10-05"
num_rooms:
type: integer
example: 1
total_price:
type: number
format: float
example: 1000.0
message:
type: string
example: "Booking details retrieved successfully."
'400':
description: Bad request. Missing parameters.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized. Invalid or expired token.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: Booking not found.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: CustomAuthToken
schemas:
ErrorResponse:
type: object
properties:
error:
type: boolean
example: true
data:
type: object
nullable: true
message:
type: string
example: "Invalid access token."
Hotel:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: "Grand Hyatt"
location:
type: string
example: "New York"
available_rooms:
type: integer
example: 10
price_per_night:
type: number
format: float
example: 250.0
BookingRequest:
type: object
properties:
hotel_id:
type: integer
example: 1
guest_name:
type: string
example: "John Doe"
check_in_date:
type: string
format: date
example: "2025-10-01"
check_out_date:
type: string
format: date
example: "2025-10-05"
num_rooms:
type: integer
example: 1
required:
- hotel_id
- guest_name
- check_in_date
- check_out_date
- num_rooms
@@ -0,0 +1 @@
google-adk==1.12
@@ -0,0 +1,6 @@
# General Agent Configuration
GOOGLE_GENAI_USE_VERTEXAI=False
GOOGLE_API_KEY=NOT_SET
GOOGLE_MODEL=gemini-2.5-flash
OAUTH_CLIENT_ID=abc123
OAUTH_CLIENT_SECRET=secret123
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

@@ -0,0 +1,263 @@
# 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.
import datetime
import logging
import sqlite3
class HotelBooker:
"""
Core business logic for hotel booking, independent of any web framework.
"""
def __init__(self, db_name="data.db"):
self.db_name = db_name
self._initialize_db()
def _get_db_connection(self):
"""Helper to get a new, independent database connection."""
conn = sqlite3.connect(self.db_name)
conn.row_factory = sqlite3.Row
return conn
def _initialize_db(self):
"""
Drops, creates, and populates the database tables with sample data.
"""
conn = None
try:
conn = self._get_db_connection()
cursor = conn.cursor()
cursor.execute("DROP TABLE IF EXISTS bookings")
cursor.execute("DROP TABLE IF EXISTS hotels")
conn.commit()
cursor.execute("""
CREATE TABLE IF NOT EXISTS hotels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
location TEXT NOT NULL,
total_rooms INTEGER NOT NULL,
available_rooms INTEGER NOT NULL,
price_per_night REAL NOT NULL
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS bookings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
custom_booking_id TEXT UNIQUE,
hotel_id INTEGER NOT NULL,
guest_name TEXT NOT NULL,
check_in_date TEXT NOT NULL,
check_out_date TEXT NOT NULL,
num_rooms INTEGER NOT NULL,
total_price REAL NOT NULL,
FOREIGN KEY (hotel_id) REFERENCES hotels(id)
)
""")
conn.commit()
sample_hotels = [
("Grand Hyatt", "New York", 200, 150, 250.00),
("The Plaza Hotel", "New York", 150, 100, 350.00),
("Hilton Chicago", "Chicago", 300, 250, 180.00),
("Marriott Marquis", "San Francisco", 250, 200, 220.00),
]
cursor.executemany(
"""
INSERT INTO hotels (name, location, total_rooms, available_rooms, price_per_night)
VALUES (?, ?, ?, ?, ?)
""",
sample_hotels,
)
conn.commit()
initial_bookings_data = [
(1, "Alice Smith", "2025-08-10", "2025-08-15", 1, 1250.00),
(3, "Bob Johnson", "2025-09-01", "2025-09-03", 2, 720.00),
]
for booking_data in initial_bookings_data:
cursor.execute(
"""
INSERT INTO bookings (hotel_id, guest_name, check_in_date, check_out_date, num_rooms, total_price)
VALUES (?, ?, ?, ?, ?, ?)
""",
booking_data,
)
booking_id_int = cursor.lastrowid
custom_id = f"HB-{booking_id_int}"
cursor.execute(
"UPDATE bookings SET custom_booking_id = ? WHERE id = ?",
(custom_id, booking_id_int),
)
conn.commit()
except sqlite3.Error as e:
if conn:
conn.rollback()
finally:
if conn:
conn.close()
def is_token_valid(self, conn, token):
"""Checks if a given token is valid and not expired."""
logging.info("not implemented")
return True
def get_available_hotels(self, cursor, location=None):
"""Retrieves a list of available hotels, optionally filtered by location."""
query = (
"SELECT id, name, location, available_rooms, price_per_night FROM"
" hotels WHERE available_rooms > 0"
)
params = []
if location:
query += " AND location LIKE ?"
params.append(f"%{location}%")
try:
cursor.execute(query, params)
rows = cursor.fetchall()
return [dict(row) for row in rows], None
except sqlite3.Error as e:
return None, f"Error getting available hotels: {e}"
def book_a_room(
self, conn, hotel_id, guest_name, check_in_date, check_out_date, num_rooms
):
"""Books a room in a specified hotel."""
cursor = conn.cursor()
try:
cursor.execute(
"SELECT available_rooms, price_per_night FROM hotels WHERE id = ?",
(hotel_id,),
)
hotel_info = cursor.fetchone()
if not hotel_info:
return None, f"Hotel with ID {hotel_id} not found."
available_rooms, price_per_night = (
hotel_info["available_rooms"],
hotel_info["price_per_night"],
)
if available_rooms < num_rooms:
return (
None,
(
f"Not enough rooms available at hotel ID {hotel_id}. Available:"
f" {available_rooms}, Requested: {num_rooms}"
),
)
try:
check_in_dt = datetime.datetime.strptime(check_in_date, "%Y-%m-%d")
check_out_dt = datetime.datetime.strptime(check_out_date, "%Y-%m-%d")
except ValueError:
return None, "Invalid date format. Please use YYYY-MM-DD."
num_nights = (check_out_dt - check_in_dt).days
if num_nights <= 0:
return None, "Check-out date must be after check-in date."
total_price = num_rooms * price_per_night * num_nights
cursor.execute(
"UPDATE hotels SET available_rooms = ? WHERE id = ?",
(available_rooms - num_rooms, hotel_id),
)
cursor.execute(
"""
INSERT INTO bookings (hotel_id, guest_name, check_in_date, check_out_date, num_rooms, total_price)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
hotel_id,
guest_name,
check_in_date,
check_out_date,
num_rooms,
total_price,
),
)
booking_id_int = cursor.lastrowid
custom_booking_id = f"HB-{booking_id_int}"
cursor.execute(
"UPDATE bookings SET custom_booking_id = ? WHERE id = ?",
(custom_booking_id, booking_id_int),
)
conn.commit()
return custom_booking_id, None
except sqlite3.Error as e:
conn.rollback()
return None, f"Error booking room: {e}"
def get_booking_details(self, cursor, booking_id=None, guest_name=None):
"""Retrieves details for a specific booking."""
query = """
SELECT
b.custom_booking_id,
h.name AS hotel_name,
h.location AS hotel_location,
b.guest_name,
b.check_in_date,
b.check_out_date,
b.num_rooms,
b.total_price
FROM
bookings b
JOIN
hotels h ON b.hotel_id = h.id
"""
params = []
result_type = "single"
if booking_id:
query += " WHERE b.custom_booking_id = ?"
params.append(booking_id)
elif guest_name:
query += " WHERE LOWER(b.guest_name) LIKE LOWER(?)"
params.append(f"%{guest_name}%")
result_type = "list"
else:
return (
None,
(
"Please provide either a booking ID or a guest name to retrieve"
" booking details."
),
)
try:
cursor.execute(query, params)
rows = cursor.fetchall()
if not rows:
return (
None,
(
f"No booking found for the given criteria (ID: {booking_id},"
f" Name: {guest_name})."
),
)
bookings = [dict(row) for row in rows]
return bookings if result_type == "list" else bookings[0], None
except sqlite3.Error as e:
return None, f"Error getting booking details: {e}"
@@ -0,0 +1,266 @@
# 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 functools import wraps
import os
import sqlite3
from dotenv import load_dotenv
from flask import Flask
from flask import g
from flask import jsonify
from flask import request
from hotelbooker_core import HotelBooker
import jwt
import requests
# Load environment variables from .env file
load_dotenv()
app = Flask(__name__)
# Instantiate the core logic class
hotel_booker = HotelBooker()
app.config["DATABASE"] = hotel_booker.db_name
OIDC_CONFIG_URL = os.environ.get(
"OIDC_CONFIG_URL", "http://localhost:5000/.well-known/openid-configuration"
)
# Cache for OIDC discovery and JWKS
oidc_config = None
jwks = None
def get_oidc_config():
"""Fetches and caches the OIDC configuration."""
global oidc_config
if oidc_config is None:
try:
response = requests.get(OIDC_CONFIG_URL)
response.raise_for_status()
oidc_config = response.json()
except requests.exceptions.RequestException as e:
return None, f"Error fetching OIDC config: {e}"
return oidc_config, None
def get_jwks():
"""Fetches and caches the JSON Web Key Set (JWKS)."""
global jwks
if jwks is None:
config, error = get_oidc_config()
if error:
return None, error
jwks_uri = config.get("jwks_uri")
if not jwks_uri:
return None, "jwks_uri not found in OIDC configuration."
try:
response = requests.get(jwks_uri)
response.raise_for_status()
jwks = response.json()
except requests.exceptions.RequestException as e:
return None, f"Error fetching JWKS: {e}"
return jwks, None
def get_db():
"""Manages a per-request database connection."""
if "db" not in g:
g.db = sqlite3.connect(app.config["DATABASE"])
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception):
db = g.pop("db", None)
if db is not None:
db.close()
def is_token_valid(token: str):
"""
Validates a JWT token using the public key from the OIDC jwks_uri.
"""
if not token:
return False, "Token is empty."
jwks_data, error = get_jwks()
if error:
return False, f"Failed to get JWKS: {error}"
try:
header = jwt.get_unverified_header(token)
kid = header.get("kid")
if not kid:
return False, "Token header missing 'kid'."
key = next(
(k for k in jwks_data.get("keys", []) if k.get("kid") == kid), None
)
if not key:
return False, "No matching key found in JWKS."
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
# The decoding happens just so that we are able to
# check if there were any exception decoding the token
# which indicate it being not valid.
# Also you could have verify_aud and verify_iss as False
# But when they are true issuer and audience are needed in the jwt.decode call
# they are checked against the values from the token
# idealy token validation should also check whether the API being called is part of
# audience so for example localhost:8081/api should cover localhost:8081/api/hotels
# but should not cover localhost:8000/admin
# so this middleware (decorator - is_token_valid, can check the request url and do that check, but we are
# skipping that as the audience will always be localhost:8081)
decoded_token = jwt.decode(
token,
key=public_key,
issuer="http://localhost:5000",
audience="http://localhost:8081",
algorithms=[header["alg"]],
options={"verify_exp": True, "verify_aud": True, "verify_iss": True},
)
return True, "Token is valid."
except jwt.ExpiredSignatureError:
return False, "Token has expired."
except jwt.InvalidAudienceError:
return False, "Invalid audience."
except jwt.InvalidIssuerError:
return False, "Invalid issuer."
except jwt.InvalidTokenError as e:
return False, f"Invalid token: {e}"
except Exception as e:
return False, f"An unexpected error occurred during token validation: {e}"
# Decorator to check for a valid access token on protected routes
def token_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
return {
"error": True,
"data": None,
"message": "Missing or invalid Authorization header.",
}, 401
token = auth_header.split(" ")[1]
is_valid, message = is_token_valid(token)
if not is_valid:
return {"error": True, "data": None, "message": message}, 401
return f(*args, **kwargs)
return decorated_function
@app.route("/hotels", methods=["GET"])
@token_required
def get_hotels():
location = request.args.get("location")
hotels, error_message = hotel_booker.get_available_hotels(
get_db().cursor(), location
)
if hotels is not None:
return (
jsonify({
"error": False,
"data": hotels,
"message": "Successfully retrieved hotels.",
}),
200,
)
else:
return jsonify({"error": True, "data": None, "message": error_message}), 500
@app.route("/book", methods=["POST"])
@token_required
def book_room():
conn = get_db()
data = request.json
hotel_id = data.get("hotel_id")
guest_name = data.get("guest_name")
check_in_date = data.get("check_in_date")
check_out_date = data.get("check_out_date")
num_rooms = data.get("num_rooms")
if not all([hotel_id, guest_name, check_in_date, check_out_date, num_rooms]):
return (
jsonify({
"error": True,
"data": None,
"message": "Missing required booking information.",
}),
400,
)
booking_id, error_message = hotel_booker.book_a_room(
conn, hotel_id, guest_name, check_in_date, check_out_date, num_rooms
)
if booking_id:
return (
jsonify({
"error": False,
"data": {"booking_id": booking_id},
"message": "Booking successful!",
}),
200,
)
else:
return jsonify({"error": True, "data": None, "message": error_message}), 400
@app.route("/booking_details", methods=["GET"])
@token_required
def get_details():
conn = get_db()
booking_id = request.args.get("booking_id")
guest_name = request.args.get("guest_name")
if not booking_id and not guest_name:
return (
jsonify({
"error": True,
"data": None,
"message": "Please provide either a booking ID or a guest name.",
}),
400,
)
details, error_message = hotel_booker.get_booking_details(
get_db().cursor(), booking_id=booking_id, guest_name=guest_name
)
if details:
return (
jsonify({
"error": False,
"data": details,
"message": "Booking details retrieved successfully.",
}),
200,
)
else:
return jsonify({"error": True, "data": None, "message": error_message}), 404
if __name__ == "__main__":
app.run(debug=True, port=8081)
@@ -0,0 +1,229 @@
openapi: 3.0.0
info:
title: Hotel Booker API
description: A simple API for managing hotel bookings, with a custom client credentials authentication flow.
version: 1.0.0
servers:
- url: http://127.0.0.1:8081
paths:
/hotels:
get:
summary: Get available hotels
description: Retrieves a list of available hotels, optionally filtered by location.
security:
- BearerAuth: []
parameters:
- in: query
name: location
schema:
type: string
description: The city to filter hotels by (e.g., 'New York').
responses:
'200':
description: Successfully retrieved hotels.
content:
application/json:
schema:
type: object
properties:
error:
type: boolean
example: false
data:
type: array
items:
$ref: '#/components/schemas/Hotel'
message:
type: string
example: "Successfully retrieved hotels."
'401':
description: Unauthorized. Invalid or expired token.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/book:
post:
summary: Book a room
description: Books a room in a specified hotel.
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BookingRequest'
responses:
'200':
description: Booking successful.
content:
application/json:
schema:
type: object
properties:
error:
type: boolean
example: false
data:
type: object
properties:
booking_id:
type: string
example: "HB-1"
message:
type: string
example: "Booking successful!"
'400':
description: Bad request. Missing information or invalid booking details.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized. Invalid or expired token.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/booking_details:
get:
summary: Get booking details
description: Retrieves details for a specific booking by ID or guest name.
security:
- BearerAuth: []
parameters:
- in: query
name: booking_id
schema:
type: string
description: The custom booking ID (e.g., 'HB-1').
- in: query
name: guest_name
schema:
type: string
description: The name of the guest to search for (partial and case-insensitive).
responses:
'200':
description: Booking details retrieved successfully.
content:
application/json:
schema:
type: object
properties:
error:
type: boolean
example: false
data:
type: object
properties:
custom_booking_id:
type: string
example: "HB-1"
hotel_name:
type: string
example: "Grand Hyatt"
hotel_location:
type: string
example: "New York"
guest_name:
type: string
example: "John Doe"
check_in_date:
type: string
example: "2025-10-01"
check_out_date:
type: string
example: "2025-10-05"
num_rooms:
type: integer
example: 1
total_price:
type: number
format: float
example: 1000.0
message:
type: string
example: "Booking details retrieved successfully."
'400':
description: Bad request. Missing parameters.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized. Invalid or expired token.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: Booking not found.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: CustomAuthToken
schemas:
ErrorResponse:
type: object
properties:
error:
type: boolean
example: true
data:
type: object
nullable: true
message:
type: string
example: "Invalid access token."
Hotel:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: "Grand Hyatt"
location:
type: string
example: "New York"
available_rooms:
type: integer
example: 10
price_per_night:
type: number
format: float
example: 250.0
BookingRequest:
type: object
properties:
hotel_id:
type: integer
example: 1
guest_name:
type: string
example: "John Doe"
check_in_date:
type: string
format: date
example: "2025-10-01"
check_out_date:
type: string
format: date
example: "2025-10-05"
num_rooms:
type: integer
example: 1
required:
- hotel_id
- guest_name
- check_in_date
- check_out_date
- num_rooms

Some files were not shown because too many files have changed in this diff Show More