Skip to content

Commit 70bf91e

Browse files
authored
Merge branch 'main' into after-agent-callback-replace
2 parents 5ab21ec + 696852a commit 70bf91e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2478
-421
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright 2025 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 . import agent
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Copyright 2025 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+
import datetime
18+
import json
19+
import re
20+
from typing import Any
21+
from zoneinfo import ZoneInfo
22+
from zoneinfo import ZoneInfoNotFoundError
23+
24+
from google.adk.agents.llm_agent import Agent
25+
from google.adk.models.lite_llm import LiteLlm
26+
from google.adk.models.lite_llm import LiteLLMClient
27+
28+
29+
class InlineJsonToolClient(LiteLLMClient):
30+
"""LiteLLM client that emits inline JSON tool calls for testing."""
31+
32+
async def acompletion(self, model, messages, tools, **kwargs):
33+
del tools, kwargs # Only needed for API parity.
34+
35+
tool_message = _find_last_role(messages, role="tool")
36+
if tool_message:
37+
tool_summary = _coerce_to_text(tool_message.get("content"))
38+
return {
39+
"id": "mock-inline-tool-final-response",
40+
"model": model,
41+
"choices": [{
42+
"message": {
43+
"role": "assistant",
44+
"content": (
45+
f"The instrumentation tool responded with: {tool_summary}"
46+
),
47+
},
48+
"finish_reason": "stop",
49+
}],
50+
"usage": {
51+
"prompt_tokens": 60,
52+
"completion_tokens": 12,
53+
"total_tokens": 72,
54+
},
55+
}
56+
57+
timezone = _extract_timezone(messages) or "Asia/Taipei"
58+
inline_call = json.dumps(
59+
{
60+
"name": "get_current_time",
61+
"arguments": {"timezone_str": timezone},
62+
},
63+
separators=(",", ":"),
64+
)
65+
66+
return {
67+
"id": "mock-inline-tool-call",
68+
"model": model,
69+
"choices": [{
70+
"message": {
71+
"role": "assistant",
72+
"content": (
73+
f"{inline_call}\nLet me double-check the clock for you."
74+
),
75+
},
76+
"finish_reason": "tool_calls",
77+
}],
78+
"usage": {
79+
"prompt_tokens": 45,
80+
"completion_tokens": 15,
81+
"total_tokens": 60,
82+
},
83+
}
84+
85+
86+
def _find_last_role(
87+
messages: list[dict[str, Any]], role: str
88+
) -> dict[str, Any]:
89+
"""Returns the last message with the given role."""
90+
for message in reversed(messages):
91+
if message.get("role") == role:
92+
return message
93+
return {}
94+
95+
96+
def _coerce_to_text(content: Any) -> str:
97+
"""Best-effort conversion from OpenAI message content to text."""
98+
if isinstance(content, str):
99+
return content
100+
if isinstance(content, dict):
101+
return _coerce_to_text(content.get("text"))
102+
if isinstance(content, list):
103+
texts = []
104+
for part in content:
105+
if isinstance(part, dict):
106+
texts.append(part.get("text") or "")
107+
elif isinstance(part, str):
108+
texts.append(part)
109+
return " ".join(text for text in texts if text)
110+
return ""
111+
112+
113+
_TIMEZONE_PATTERN = re.compile(r"([A-Za-z]+/[A-Za-z_]+)")
114+
115+
116+
def _extract_timezone(messages: list[dict[str, Any]]) -> str | None:
117+
"""Extracts an IANA timezone string from the last user message."""
118+
user_message = _find_last_role(messages, role="user")
119+
text = _coerce_to_text(user_message.get("content"))
120+
if not text:
121+
return None
122+
match = _TIMEZONE_PATTERN.search(text)
123+
if match:
124+
return match.group(1)
125+
lowered = text.lower()
126+
if "taipei" in lowered:
127+
return "Asia/Taipei"
128+
if "new york" in lowered:
129+
return "America/New_York"
130+
if "london" in lowered:
131+
return "Europe/London"
132+
if "tokyo" in lowered:
133+
return "Asia/Tokyo"
134+
return None
135+
136+
137+
def get_current_time(timezone_str: str) -> dict[str, str]:
138+
"""Returns mock current time for the provided timezone."""
139+
try:
140+
tz = ZoneInfo(timezone_str)
141+
except ZoneInfoNotFoundError as exc:
142+
return {
143+
"status": "error",
144+
"report": f"Unable to parse timezone '{timezone_str}': {exc}",
145+
}
146+
now = datetime.datetime.now(tz)
147+
return {
148+
"status": "success",
149+
"report": (
150+
f"The current time in {timezone_str} is"
151+
f" {now.strftime('%Y-%m-%d %H:%M:%S %Z')}."
152+
),
153+
}
154+
155+
156+
_mock_model = LiteLlm(
157+
model="mock/inline-json-tool-calls",
158+
llm_client=InlineJsonToolClient(),
159+
)
160+
161+
root_agent = Agent(
162+
name="litellm_inline_tool_tester",
163+
model=_mock_model,
164+
description=(
165+
"Demonstrates LiteLLM inline JSON tool-call parsing without an external"
166+
" VLLM deployment."
167+
),
168+
instruction=(
169+
"You are a deterministic clock assistant. Always call the"
170+
" get_current_time tool before answering user questions. After the tool"
171+
" responds, summarize what it returned."
172+
),
173+
tools=[get_current_time],
174+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright 2025 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 numpy as np
15+
import sounddevice as sd
16+
17+
# input audio example. replace with the input audio you want to test
18+
FILE_PATH = 'adk_live_audio_storage_input_audio_1762910896736.pcm'
19+
# output audio example. replace with the input audio you want to test
20+
FILE_PATH = 'adk_live_audio_storage_output_audio_1762910893258.pcm;rate=24000'
21+
# PCM rate is always 24,000 for input and output
22+
SAMPLE_RATE = 24000
23+
CHANNELS = 1
24+
DTYPE = np.int16 # Common types: int16, float32
25+
26+
# Read and play
27+
with open(FILE_PATH, 'rb') as f:
28+
# Load raw data into numpy array
29+
raw_data = f.read()
30+
audio_array = np.frombuffer(raw_data, dtype=DTYPE)
31+
32+
# Reshape if stereo (interleaved)
33+
if CHANNELS > 1:
34+
audio_array = audio_array.reshape((-1, CHANNELS))
35+
36+
# Play
37+
print('Playing...')
38+
sd.play(audio_array, SAMPLE_RATE)
39+
sd.wait()

contributing/samples/live_bidi_streaming_single_agent/agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ async def check_prime(nums: list[int]) -> str:
6565

6666

6767
root_agent = Agent(
68-
model='gemini-live-2.5-flash-preview-native-audio-09-2025', # vertex
69-
# model='gemini-2.5-flash-native-audio-preview-09-2025', # for AI studio
68+
# model='gemini-live-2.5-flash-preview-native-audio-09-2025', # vertex
69+
model='gemini-2.5-flash-native-audio-preview-09-2025', # for AI studio
7070
# key
7171
name='roll_dice_agent',
7272
description=(

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ classifiers = [ # List of https://pypi.org/classifiers/
1414
"Intended Audience :: Science/Research",
1515
"Programming Language :: Python",
1616
"Programming Language :: Python :: 3",
17-
"Programming Language :: Python :: 3.9",
1817
"Programming Language :: Python :: 3.10",
1918
"Programming Language :: Python :: 3.11",
2019
"Programming Language :: Python :: 3.12",
2120
"Programming Language :: Python :: 3.13",
21+
"Programming Language :: Python :: 3.14",
2222
"Operating System :: OS Independent",
2323
"Topic :: Software Development :: Libraries :: Python Modules",
2424
"License :: OSI Approved :: Apache Software License",
@@ -43,6 +43,7 @@ dependencies = [
4343
"google-cloud-storage>=3.0.0, <4.0.0", # For GCS Artifact service
4444
"google-genai>=1.45.0, <2.0.0", # Google GenAI SDK
4545
"graphviz>=0.20.2, <1.0.0", # Graphviz for graph rendering
46+
"jsonschema>=4.23.0, <5.0.0", # Agent Builder config validation
4647
"mcp>=1.8.0, <2.0.0;python_version>='3.10'", # For MCP Toolset
4748
"opentelemetry-api>=1.37.0, <=1.37.0", # OpenTelemetry - limit upper version for sdk and api to not risk breaking changes from unstable _logs package.
4849
"opentelemetry-exporter-gcp-logging>=1.9.0a0, <2.0.0",
@@ -115,7 +116,7 @@ test = [
115116
# go/keep-sorted start
116117
"a2a-sdk>=0.3.0,<0.4.0;python_version>='3.10'",
117118
"anthropic>=0.43.0", # For anthropic model tests
118-
"crewai[tools];python_version>='3.10'", # For CrewaiTool tests
119+
"crewai[tools];python_version>='3.10' and python_version<'3.14'", # For CrewaiTool tests
119120
"kubernetes>=29.0.0", # For GkeCodeExecutor
120121
"langchain-community>=0.3.17",
121122
"langgraph>=0.2.60, <0.4.8", # For LangGraphAgent
@@ -145,7 +146,7 @@ docs = [
145146
extensions = [
146147
"anthropic>=0.43.0", # For anthropic model support
147148
"beautifulsoup4>=3.2.2", # For load_web_page tool.
148-
"crewai[tools];python_version>='3.10'", # For CrewaiTool
149+
"crewai[tools];python_version>='3.10' and python_version<'3.14'", # For CrewaiTool
149150
"docker>=7.0.0", # For ContainerCodeExecutor
150151
"kubernetes>=29.0.0", # For GkeCodeExecutor
151152
"langgraph>=0.2.60, <0.4.8", # For LangGraphAgent

0 commit comments

Comments
 (0)