Skip to content

Commit f1fa856

Browse files
Merge branch 'main' into custom-session-id-with-agent-engine
2 parents 781e4ef + bbad9ec commit f1fa856

File tree

11 files changed

+525
-13
lines changed

11 files changed

+525
-13
lines changed

contributing/samples/hello_world/main.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,15 @@ async def run_prompt(session: Session, new_message: str):
4848
session_id=session.id,
4949
new_message=content,
5050
):
51-
if event.content.parts and event.content.parts[0].text:
52-
print(f'** {event.author}: {event.content.parts[0].text}')
51+
if event.content.parts:
52+
for part in event.content.parts:
53+
if part.text:
54+
print(f'** {event.author}: {part.text}')
55+
if part.function_call:
56+
print(
57+
f'** {event.author} calls tool: {part.function_call.name} with'
58+
f' args {part.function_call.args}'
59+
)
5360

5461
async def run_prompt_bytes(session: Session, new_message: str):
5562
content = types.Content(
@@ -74,6 +81,7 @@ async def check_rolls_in_state(rolls_size: int):
7481
session = await runner.session_service.get_session(
7582
app_name=app_name, user_id=user_id_1, session_id=session_11.id
7683
)
84+
print('** session.state:', session.state)
7785
assert len(session.state['rolls']) == rolls_size
7886
for roll in session.state['rolls']:
7987
assert roll > 0 and roll <= 100
@@ -82,9 +90,9 @@ async def check_rolls_in_state(rolls_size: int):
8290
print('Start time:', start_time)
8391
print('------------------------------------')
8492
await run_prompt(session_11, 'Hi')
85-
await run_prompt(session_11, 'Roll a die with 100 sides')
93+
await run_prompt(session_11, 'Roll a dice with 100 sides')
8694
await check_rolls_in_state(1)
87-
await run_prompt(session_11, 'Roll a die again with 100 sides.')
95+
await run_prompt(session_11, 'Roll a dice again with 100 sides.')
8896
await check_rolls_in_state(2)
8997
await run_prompt(session_11, 'What numbers did I got?')
9098
await run_prompt_bytes(session_11, 'Hi bytes')

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ async def _ensure_httpx_client(self) -> httpx.AsyncClient:
233233
httpx_client=self._httpx_client,
234234
streaming=False,
235235
polling=False,
236-
supported_transports=[A2ATransport.jsonrpc],
236+
supported_transports=[A2ATransport.jsonrpc, A2ATransport.http_json],
237237
)
238238
self._a2a_client_factory = A2AClientFactory(config=client_config)
239239
return self._httpx_client

src/google/adk/cli/adk_web_server.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
from ..agents.base_agent import BaseAgent
6161
from ..agents.live_request_queue import LiveRequest
6262
from ..agents.live_request_queue import LiveRequestQueue
63+
from ..agents.llm_agent import LlmAgent
64+
from ..agents.llm_agent import ToolUnion
6365
from ..agents.run_config import RunConfig
6466
from ..agents.run_config import StreamingMode
6567
from ..apps.app import App
@@ -90,7 +92,10 @@
9092
from ..runners import Runner
9193
from ..sessions.base_session_service import BaseSessionService
9294
from ..sessions.session import Session
95+
from ..utils.agent_info import AgentInfo
96+
from ..utils.agent_info import get_agents_dict
9397
from ..utils.context_utils import Aclosing
98+
from ..utils.feature_decorator import experimental
9499
from ..version import __version__
95100
from .cli_eval import EVAL_SESSION_ID_PREFIX
96101
from .utils import cleanup
@@ -489,6 +494,7 @@ class AppInfo(common.BaseModel):
489494
description: str
490495
language: Literal["yaml", "python"]
491496
is_computer_use: bool = False
497+
agents: Optional[dict[str, AgentInfo]] = None
492498

493499

494500
class ListAppsResponse(common.BaseModel):
@@ -958,6 +964,25 @@ async def list_apps(
958964
return ListAppsResponse(apps=[AppInfo(**app) for app in apps_info])
959965
return self.agent_loader.list_agents()
960966

967+
@experimental
968+
@app.get("/apps/{app_name}/app-info", response_model_exclude_none=True)
969+
async def get_adk_app_info(app_name: str) -> AppInfo:
970+
"""Returns the detailed info for a given ADK app."""
971+
agent_or_app = self.agent_loader.load_agent(app_name)
972+
root_agent = self._get_root_agent(agent_or_app)
973+
if isinstance(root_agent, LlmAgent):
974+
return AppInfo(
975+
name=app_name,
976+
root_agent_name=root_agent.name,
977+
description=root_agent.description,
978+
language="python",
979+
agents=get_agents_dict(root_agent),
980+
)
981+
else:
982+
raise HTTPException(
983+
status_code=400, detail="Root agent is not an LlmAgent"
984+
)
985+
961986
@app.get("/debug/trace/{event_id}", tags=[TAG_DEBUG])
962987
async def get_trace_dict(event_id: str) -> Any:
963988
event_dict = trace_dict.get(event_id, None)

src/google/adk/cli/cli_create.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@
6464

6565
_EXPRESS_TOS_MSG = """
6666
Google Cloud Express Mode Terms of Service: https://cloud.google.com/terms/google-cloud-express
67-
By continuing, you agree to the Terms of Service for Vertex AI Express Mode.
67+
By using this application, you agree to the Google Cloud Express Mode terms of service and any
68+
applicable services and APIs. You also agree to only use this application for your trade,
69+
business, craft, or profession.
70+
6871
Would you like to proceed? (yes/no)
6972
"""
7073

src/google/adk/cli/cli_deploy.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,13 @@ def to_agent_engine(
949949

950950
click.echo('Resolving files and dependencies...')
951951
agent_config = {}
952+
if agent_engine_config_file and not os.path.exists(
953+
agent_engine_config_file
954+
):
955+
raise click.ClickException(
956+
'Agent engine config file not found: '
957+
f'{parent_folder}/{agent_engine_config_file}'
958+
)
952959
if not agent_engine_config_file:
953960
# Attempt to read the agent engine config from .agent_engine_config.json in the dir (if any).
954961
agent_engine_config_file = os.path.join(

src/google/adk/tools/skill_toolset.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,15 @@ def _build_wrapper_code(
510510

511511
if ext == "py":
512512
argv_list = [script_path]
513-
for k, v in script_args.items():
513+
for k, v in script_args.get("args", {}).items():
514514
argv_list.extend([f"--{k}", str(v)])
515+
for k, v in script_args.get("short_options", {}).items():
516+
argv_list.extend([f"-{k}", str(v)])
517+
positional_args = script_args.get("positional_args", [])
518+
if positional_args:
519+
argv_list.append("--")
520+
for v in positional_args:
521+
argv_list.append(str(v))
515522
code_lines.extend([
516523
f" sys.argv = {argv_list!r}",
517524
" try:",
@@ -522,8 +529,15 @@ def _build_wrapper_code(
522529
])
523530
elif ext in ("sh", "bash"):
524531
arr = ["bash", script_path]
525-
for k, v in script_args.items():
532+
for k, v in script_args.get("args", {}).items():
526533
arr.extend([f"--{k}", str(v)])
534+
for k, v in script_args.get("short_options", {}).items():
535+
arr.extend([f"-{k}", str(v)])
536+
positional_args = script_args.get("positional_args", [])
537+
if positional_args:
538+
arr.append("--")
539+
for v in positional_args:
540+
arr.append(str(v))
527541
timeout = self._script_timeout
528542
code_lines.extend([
529543
" try:",
@@ -590,8 +604,23 @@ def _get_declaration(self) -> types.FunctionDeclaration | None:
590604
"args": {
591605
"type": "object",
592606
"description": (
593-
"Optional arguments to pass to the script as key-value"
594-
" pairs."
607+
"Optional arguments to pass as long options (e.g.,"
608+
" {'n': 5} becomes --n 5)."
609+
),
610+
},
611+
"short_options": {
612+
"type": "object",
613+
"description": (
614+
"Optional SHORT options to pass (e.g., {'n': 5}"
615+
" becomes -n 5)."
616+
),
617+
},
618+
"positional_args": {
619+
"type": "array",
620+
"items": {"type": "string"},
621+
"description": (
622+
"Optional list of positional arguments in exact order"
623+
" (e.g., ['input.txt', 'output.txt'])."
595624
),
596625
},
597626
},
@@ -604,16 +633,38 @@ async def run_async(
604633
) -> Any:
605634
skill_name = args.get("skill_name")
606635
script_path = args.get("script_path")
607-
script_args = args.get("args", {})
608-
if not isinstance(script_args, dict):
636+
script_args = {
637+
"args": args.get("args", {}),
638+
"short_options": args.get("short_options", {}),
639+
"positional_args": args.get("positional_args", []),
640+
}
641+
if not isinstance(script_args["args"], dict):
609642
return {
610643
"error": (
611644
"'args' must be a JSON object (key-value pairs),"
612-
f" got {type(script_args).__name__}."
645+
f" got {type(script_args['args']).__name__}."
613646
),
614647
"error_code": "INVALID_ARGS_TYPE",
615648
}
616649

650+
if not isinstance(script_args["short_options"], dict):
651+
return {
652+
"error": (
653+
"'short_options' must be a JSON object (key-value pairs),"
654+
f" got {type(script_args['short_options']).__name__}."
655+
),
656+
"error_code": "INVALID_SHORT_OPTIONS_TYPE",
657+
}
658+
659+
if not isinstance(script_args["positional_args"], list):
660+
return {
661+
"error": (
662+
"'positional_args' must be a JSON array (list),"
663+
f" got {type(script_args['positional_args']).__name__}."
664+
),
665+
"error_code": "INVALID_POSITIONAL_ARGS_TYPE",
666+
}
667+
617668
if not skill_name:
618669
return {
619670
"error": "Skill name is required.",

src/google/adk/utils/agent_info.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 __future__ import annotations
16+
17+
from typing import Any
18+
19+
from google.genai import types
20+
import pydantic
21+
22+
from ..agents.llm_agent import LlmAgent
23+
from ..agents.llm_agent import ToolUnion
24+
from ..tools.base_tool import BaseTool
25+
from ..tools.base_toolset import BaseToolset
26+
from ..tools.function_tool import FunctionTool
27+
28+
29+
class AgentInfo(pydantic.BaseModel):
30+
name: str
31+
description: str
32+
instruction: str
33+
tools: list[types.Tool]
34+
sub_agents: list[str]
35+
36+
37+
def get_tools_info(tools: list[ToolUnion]) -> list[Any]:
38+
"""Returns the info for a given list of tools."""
39+
final_tools = []
40+
for tool in tools:
41+
if isinstance(tool, BaseTool):
42+
final_tools.append(tool)
43+
elif isinstance(tool, BaseToolset):
44+
final_tools.extend(tool.get_tools())
45+
else:
46+
final_tools.append(FunctionTool(tool))
47+
return [
48+
types.Tool(function_declarations=[tool._get_declaration()])
49+
for tool in final_tools
50+
if tool._get_declaration()
51+
]
52+
53+
54+
def get_agents_dict(agent: LlmAgent) -> dict[str, AgentInfo]:
55+
"""Returns a dict with info for the agent and its sub-agents."""
56+
agents_dict = {}
57+
58+
def _traverse(current_agent: LlmAgent):
59+
if current_agent.name in agents_dict:
60+
return
61+
62+
sub_agent_names = []
63+
for sub_agent in current_agent.sub_agents:
64+
if isinstance(sub_agent, LlmAgent):
65+
_traverse(sub_agent)
66+
sub_agent_names.append(sub_agent.name)
67+
68+
agents_dict[current_agent.name] = AgentInfo(
69+
name=current_agent.name,
70+
description=current_agent.description,
71+
instruction=current_agent.instruction,
72+
tools=get_tools_info(current_agent.tools),
73+
sub_agents=sub_agent_names,
74+
)
75+
76+
_traverse(agent)
77+
return agents_dict

tests/unittests/agents/test_remote_a2a_agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from a2a.types import TaskStatus as A2ATaskStatus
3636
from a2a.types import TaskStatusUpdateEvent
3737
from a2a.types import TextPart
38+
from a2a.types import TransportProtocol as A2ATransport
3839
from google.adk.a2a.agent import ParametersConfig
3940
from google.adk.a2a.agent import RequestInterceptor
4041
from google.adk.a2a.agent.config import A2aRemoteAgentConfig
@@ -198,6 +199,10 @@ async def test_ensure_httpx_client_creates_new_client(self):
198199
assert client is not None
199200
assert agent._httpx_client == client
200201
assert agent._httpx_client_needs_cleanup is True
202+
assert agent._a2a_client_factory._config.supported_transports == [
203+
A2ATransport.jsonrpc,
204+
A2ATransport.http_json,
205+
]
201206

202207
@pytest.mark.asyncio
203208
async def test_ensure_httpx_client_reuses_existing_client(self):

0 commit comments

Comments
 (0)