Skip to content

Commit ee8c179

Browse files
authored
Merge branch 'main' into feat/evaluate-full-response
2 parents dd72e3a + e3567a6 commit ee8c179

18 files changed

Lines changed: 420 additions & 36 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Local Environment Skill Sample
2+
3+
This sample demonstrates how to use the `LocalEnvironment` with the `EnvironmentToolset` to allow an agent to manually discover and load skills from the environment, rather than using the pre-configured `SkillToolset`.
4+
5+
## Description
6+
7+
The agent is configured with the `EnvironmentToolset` and is initialized with a `LocalEnvironment` pointing to the agent's directory.
8+
Instead of having skills pre-loaded, the agent uses system instructions that guide it to search for skills in the `skills/` folder and load them by reading their `SKILL.md` files using the `ReadFile` tool.
9+
10+
This demonstrates a "manual skill loading" pattern where the agent can acquire new capabilities dynamically by reading instructions from the environment.
11+
12+
## Sample Usage
13+
14+
You can interact with the agent by providing prompts that require a specific skill (like weather).
15+
16+
### Example Prompt
17+
18+
> "Can you check the weather in Sunnyvale?"
19+
20+
### Expected Behavior
21+
22+
1. **Find Skill**: The agent uses the `Execute` tool to search for all available skills by running `find skills -name SKILL.md`.
23+
2. **Load Skill**: The agent identifies the relevant skill and uses the `ReadFile` tool to read its `SKILL.md` file.
24+
3. **Execute Skill**: The agent follows the instructions in the skill file (e.g., reading references or running scripts) to answer the user's request.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import pathlib
15+
16+
from google.adk import Agent
17+
from google.adk.environment import LocalEnvironment
18+
from google.adk.tools.base_tool import BaseTool
19+
from google.adk.tools.environment import EnvironmentToolset
20+
from google.genai import types
21+
22+
23+
class GetTimezoneTool(BaseTool):
24+
"""A tool to get the timezone for a given location."""
25+
26+
def __init__(self):
27+
super().__init__(
28+
name="get_timezone",
29+
description="Returns the timezone for a given location.",
30+
)
31+
32+
def _get_declaration(self) -> types.FunctionDeclaration | None:
33+
return types.FunctionDeclaration(
34+
name=self.name,
35+
description=self.description,
36+
parameters_json_schema={
37+
"type": "object",
38+
"properties": {
39+
"location": {
40+
"type": "string",
41+
"description": "The location to get the timezone for.",
42+
},
43+
},
44+
"required": ["location"],
45+
},
46+
)
47+
48+
async def run_async(self, *, args: dict, tool_context) -> str:
49+
return f"The timezone for {args['location']} is UTC+00:00."
50+
51+
52+
def get_wind_speed(location: str) -> str:
53+
"""Returns the current wind speed for a given location."""
54+
return f"The wind speed in {location} is 10 mph."
55+
56+
57+
BASE_INSTRUCTION = (
58+
"You are a helpful AI assistant that can use the local environment to"
59+
" execute commands and file I/O."
60+
)
61+
62+
SKILL_USAGE_INSTRUCTION = """\
63+
[SKILLS ACCESS]
64+
You have access to specialized skills stored in the environment's `skills/` folder.
65+
Each skill is a folder containing a `SKILL.md` file with instructions.
66+
67+
[MANDATORY PROCEDURE]
68+
Before declaring that you cannot perform a task or answer a question (especially for domain-specific queries like weather), you MUST:
69+
1. Use the `Execute` tool to search for all available skills by running: `find skills -name SKILL.md`
70+
2. Review the list of found skills to see if any are relevant to the user's request.
71+
3. If a relevant skill is found, use the `ReadFile` tool to read its `SKILL.md` file.
72+
4. Follow the instructions in that file to complete the request.
73+
*CRITICAL NOTE ON PATHS:* All file and script paths mentioned inside a `SKILL.md` file (e.g., `references/...` or `scripts/...`) are RELATIVE to that specific skill's folder. You MUST resolve them by prepending the skill's folder path (e.g., if the skill is at `skills/weather-skill/`, you must read `skills/weather-skill/references/weather_info.md`).
74+
75+
Failure to check the `skills/` directory before stating you cannot help is unacceptable.\
76+
"""
77+
78+
79+
root_agent = Agent(
80+
model="gemini-2.5-pro",
81+
name="local_environment_skill_agent",
82+
description=(
83+
"An agent that uses local environment tools to load and use skills."
84+
),
85+
instruction=f"{BASE_INSTRUCTION}\n\n{SKILL_USAGE_INSTRUCTION}",
86+
tools=[
87+
EnvironmentToolset(
88+
environment=LocalEnvironment(
89+
working_dir=pathlib.Path(__file__).parent
90+
),
91+
),
92+
GetTimezoneTool(),
93+
get_wind_speed,
94+
],
95+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
name: weather-skill
3+
description: A skill that provides weather information based on reference data.
4+
metadata:
5+
adk_additional_tools:
6+
- get_wind_speed
7+
---
8+
9+
Step 1: Check 'references/weather_info.md' for the current weather.
10+
Step 2: If humidity is requested, use run 'scripts/get_humidity.py' with the `location` argument.
11+
Step 3: If wind speed is requested, use the `get_wind_speed` tool.
12+
Step 4: Provide the update to the user.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Weather Information
2+
3+
- **Location:** San Francisco, CA
4+
- **Condition:** Sunny ☀️
5+
- **Temperature:** 72°F (22°C)
6+
- **Forecast:** Clear skies all day.
7+
8+
- **Location:** Sunnyvale, CA
9+
- **Condition:** Sunny ☀️
10+
- **Temperature:** 75°F (24°C)
11+
- **Forecast:** Warm and sunny.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import argparse
16+
17+
18+
def get_humidity(location: str) -> str:
19+
"""Fetch live humidity for a given location. (Simulated)"""
20+
print(f"Fetching live humidity for {location}...")
21+
return "45% (Simulated)"
22+
23+
24+
if __name__ == "__main__":
25+
parser = argparse.ArgumentParser()
26+
parser.add_argument("--location", type=str, default="Mountain View")
27+
args = parser.parse_args()
28+
29+
print(get_humidity(args.location))

src/google/adk/a2a/executor/a2a_agent_executor.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,17 +260,15 @@ async def _handle_request(
260260
context.context_id,
261261
self._config.gen_ai_part_converter,
262262
):
263-
a2a_event = await execute_after_event_interceptors(
263+
a2a_events = await execute_after_event_interceptors(
264264
a2a_event,
265265
executor_context,
266266
adk_event,
267267
self._config.execute_interceptors,
268268
)
269-
if a2a_event is None:
270-
continue
271-
272-
task_result_aggregator.process_event(a2a_event)
273-
await event_queue.enqueue_event(a2a_event)
269+
for e in a2a_events:
270+
task_result_aggregator.process_event(e)
271+
await event_queue.enqueue_event(e)
274272

275273
# publish the task result event - this is final
276274
if (

src/google/adk/a2a/executor/a2a_agent_executor_impl.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from ..experimental import a2a_experimental
5050
from .config import A2aAgentExecutorConfig
5151
from .executor_context import ExecutorContext
52+
from .interceptors.include_artifacts_in_a2a_event import include_artifacts_in_a2a_event_interceptor
5253
from .utils import execute_after_agent_interceptors
5354
from .utils import execute_after_event_interceptors
5455
from .utils import execute_before_agent_interceptors
@@ -221,15 +222,14 @@ async def _handle_request(
221222
self._config.gen_ai_part_converter,
222223
):
223224
a2a_event.metadata = self._get_invocation_metadata(executor_context)
224-
a2a_event = await execute_after_event_interceptors(
225+
a2a_events = await execute_after_event_interceptors(
225226
a2a_event,
226227
executor_context,
227228
adk_event,
228229
self._config.execute_interceptors,
229230
)
230-
if not a2a_event:
231-
continue
232-
await event_queue.enqueue_event(a2a_event)
231+
for e in a2a_events:
232+
await event_queue.enqueue_event(e)
233233

234234
if error_event:
235235
final_event = error_event

src/google/adk/a2a/executor/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class ExecuteInterceptor:
5757
after_event: Optional[
5858
Callable[
5959
[ExecutorContext, A2AEvent, Event],
60-
Awaitable[Union[A2AEvent, None]],
60+
Awaitable[Union[A2AEvent, list[A2AEvent], None]],
6161
]
6262
] = None
6363
"""Hook executed after an ADK event is converted to an A2A event.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from .include_artifacts_in_a2a_event import include_artifacts_in_a2a_event_interceptor
16+
17+
__all__ = [
18+
"include_artifacts_in_a2a_event_interceptor",
19+
]

0 commit comments

Comments
 (0)