chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# Reflect And Retry Tool Plugin
|
||||
|
||||
`ReflectAndRetryToolPlugin` provides self-healing, concurrent-safe error
|
||||
recovery for tool failures.
|
||||
|
||||
**Key Features:**
|
||||
|
||||
- **Concurrency Safe:** Uses locking to safely handle parallel tool
|
||||
executions
|
||||
- **Configurable Scope:** Tracks failures per-invocation (default) or globally
|
||||
using the `TrackingScope` enum.
|
||||
- **Extensible Scoping:** The `_get_scope_key` method can be overridden to
|
||||
implement custom tracking logic (e.g., per-user or per-session).
|
||||
- **Granular Tracking:** Failure counts are tracked per-tool within the
|
||||
defined scope. A success with one tool resets its counter without affecting
|
||||
others.
|
||||
- **Custom Error Extraction:** Supports detecting errors in normal tool
|
||||
responses that don't throw exceptions, by overriding the
|
||||
`extract_error_from_result` method.
|
||||
|
||||
## Samples
|
||||
|
||||
Here are some sample agents to demonstrate the usage of the plugin.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
This is a hello world example to show the basic usage of the plugin. The
|
||||
`guess_number_tool` is hacked with both Exceptions and error responses. With the
|
||||
help of the `CustomRetryPlugin`, both above error types can lead to retries.
|
||||
|
||||
For example, here is the output from agent:
|
||||
|
||||
```
|
||||
I'll guess the number 50. Let's see how it is!
|
||||
My guess of 50 was too high! I'll try a smaller number this time. Let's go with 25.
|
||||
My guess of 25 was still too high! I'm going smaller. How about 10?
|
||||
Still too high! My guess of 10 was also too large. I'll try 5 this time.
|
||||
My guess of 5 is "almost valid"! That's good news, it means I'm getting very close. I'll try 4.
|
||||
My guess of 4 is still "almost valid," just like 5. It seems I'm still hovering around the right answer. Let's try 3!
|
||||
I guessed the number 3, and it is valid! I found it!
|
||||
```
|
||||
|
||||
You can run the agent with:
|
||||
|
||||
```bash
|
||||
$ adk web contributing/samples/plugin_reflect_tool_retry
|
||||
```
|
||||
|
||||
Select "basic" and provide the following prompt to see the agent retrying tool
|
||||
calls:
|
||||
|
||||
```
|
||||
Please guess a number! Tell me what number you guess and how is it.
|
||||
```
|
||||
|
||||
### Hallucinating tool calls
|
||||
|
||||
The "hallucinating_func_name" agent is an example to show the plugin can retry
|
||||
hallucinating tool calls.
|
||||
|
||||
For example, we used the `after_model_callback` to hack a tool call with the
|
||||
wrong name then the agent can retry calling with the right tool name.
|
||||
|
||||
You can run the agent with:
|
||||
|
||||
```bash
|
||||
$ adk web contributing/samples/plugin_reflect_tool_retry
|
||||
```
|
||||
|
||||
Select "hallucinating_func_name" and provide the following prompt to see the
|
||||
agent retrying tool calls:
|
||||
|
||||
```
|
||||
Roll a 6 sided die
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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,83 @@
|
||||
# Copyright 2026 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 typing import Any
|
||||
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.apps.app import App
|
||||
from google.adk.plugins import LoggingPlugin
|
||||
from google.adk.plugins import ReflectAndRetryToolPlugin
|
||||
|
||||
APP_NAME = "basic"
|
||||
USER_ID = "test_user"
|
||||
|
||||
|
||||
def guess_number_tool(query: int) -> dict[str, Any]:
|
||||
"""A tool that guesses a number.
|
||||
|
||||
Args:
|
||||
query: The number to guess.
|
||||
|
||||
Returns:
|
||||
A dictionary containing the status and result of the tool execution.
|
||||
"""
|
||||
target_number = 3
|
||||
if query == target_number:
|
||||
return {"status": "success", "result": "Number is valid."}
|
||||
|
||||
if abs(query - target_number) <= 2:
|
||||
return {"status": "error", "error_message": "Number is almost valid."}
|
||||
|
||||
if query > target_number:
|
||||
raise ValueError("Number is too large.")
|
||||
|
||||
if query < target_number:
|
||||
raise ValueError("Number is too small.")
|
||||
|
||||
raise ValueError("Number is invalid.")
|
||||
|
||||
|
||||
class CustomRetryPlugin(ReflectAndRetryToolPlugin):
|
||||
|
||||
async def extract_error_from_result(
|
||||
self, *, tool, tool_args, tool_context, result
|
||||
):
|
||||
return result if result.get("status") == "error" else None
|
||||
|
||||
|
||||
# Sample query: "guess a number between 1 and 50"
|
||||
root_agent = LlmAgent(
|
||||
name="hello_world",
|
||||
description="Helpful agent",
|
||||
instruction="""Your goal is to guess a secret positive integer by using the
|
||||
`guess_number_tool`.
|
||||
The tool will provide feedback on each guess.
|
||||
Your objective is to keep guessing until guess_number_tool returns
|
||||
'status: success'.
|
||||
Start by guessing 50, and use the tool's feedback to adjust your guesses
|
||||
and find the target number.""",
|
||||
tools=[guess_number_tool],
|
||||
)
|
||||
|
||||
|
||||
app = App(
|
||||
name=APP_NAME,
|
||||
root_agent=root_agent,
|
||||
plugins=[
|
||||
CustomRetryPlugin(
|
||||
max_retries=20, throw_exception_if_retry_exceeded=False
|
||||
),
|
||||
LoggingPlugin(),
|
||||
],
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Copyright 2026 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
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# Copyright 2026 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 random
|
||||
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.apps.app import App
|
||||
from google.adk.models.llm_response import LlmResponse
|
||||
from google.adk.plugins import ReflectAndRetryToolPlugin
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
APP_NAME = "hallucinating_func_name"
|
||||
USER_ID = "test_user"
|
||||
|
||||
hallucinated = False # Whether the tool name is hallucinated
|
||||
|
||||
|
||||
def roll_die(sides: int, tool_context: ToolContext) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
result = random.randint(1, sides)
|
||||
if not "rolls" in tool_context.state:
|
||||
tool_context.state["rolls"] = []
|
||||
|
||||
tool_context.state["rolls"] = tool_context.state["rolls"] + [result]
|
||||
return result
|
||||
|
||||
|
||||
def after_model_callback(
|
||||
callback_context: CallbackContext, llm_response: LlmResponse
|
||||
):
|
||||
"""After model callback to produce one hallucinating tool call."""
|
||||
global hallucinated
|
||||
|
||||
if hallucinated:
|
||||
return None
|
||||
|
||||
if (
|
||||
llm_response.content
|
||||
and llm_response.content.parts
|
||||
and llm_response.content.parts[0].function_call
|
||||
and llm_response.content.parts[0].function_call.name == "roll_die"
|
||||
):
|
||||
llm_response.content.parts[0].function_call.name = "roll_die_wrong_name"
|
||||
hallucinated = True
|
||||
return None
|
||||
|
||||
|
||||
root_agent = LlmAgent(
|
||||
name="hello_world",
|
||||
description="Helpful agent",
|
||||
instruction="""Use guess_number_tool to guess a number.""",
|
||||
tools=[roll_die],
|
||||
after_model_callback=after_model_callback,
|
||||
)
|
||||
|
||||
|
||||
app = App(
|
||||
name=APP_NAME,
|
||||
root_agent=root_agent,
|
||||
plugins=[
|
||||
ReflectAndRetryToolPlugin(max_retries=3),
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user