Skip to content

Commit d197e74

Browse files
authored
Merge branch 'main' into feature/ollama-llm
2 parents bab8fb3 + f90adff commit d197e74

File tree

2 files changed

+224
-1
lines changed

2 files changed

+224
-1
lines changed

src/google/adk/cli/adk_web_server.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,9 @@ async def run_agent_live(
15411541
modalities: List[Literal["TEXT", "AUDIO"]] = Query(
15421542
default=["AUDIO"]
15431543
), # Only allows "TEXT" or "AUDIO"
1544+
proactive_audio: bool | None = Query(default=None),
1545+
enable_affective_dialog: bool | None = Query(default=None),
1546+
enable_session_resumption: bool | None = Query(default=None),
15441547
) -> None:
15451548
await websocket.accept()
15461549

@@ -1557,7 +1560,22 @@ async def run_agent_live(
15571560

15581561
async def forward_events():
15591562
runner = await self.get_runner_async(app_name)
1560-
run_config = RunConfig(response_modalities=modalities)
1563+
run_config = RunConfig(
1564+
response_modalities=modalities,
1565+
proactivity=(
1566+
types.ProactivityConfig(proactive_audio=proactive_audio)
1567+
if proactive_audio is not None
1568+
else None
1569+
),
1570+
enable_affective_dialog=enable_affective_dialog,
1571+
session_resumption=(
1572+
types.SessionResumptionConfig(
1573+
transparent=enable_session_resumption
1574+
)
1575+
if enable_session_resumption is not None
1576+
else None
1577+
),
1578+
)
15611579
async with Aclosing(
15621580
runner.run_live(
15631581
session=session,
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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 asyncio
16+
import types
17+
from typing import Any
18+
19+
from fastapi.testclient import TestClient
20+
from google.adk.agents.base_agent import BaseAgent
21+
from google.adk.cli.adk_web_server import AdkWebServer
22+
from google.adk.events.event import Event
23+
from google.adk.sessions.in_memory_session_service import InMemorySessionService
24+
import pytest
25+
26+
27+
class _DummyAgent(BaseAgent):
28+
29+
def __init__(self) -> None:
30+
super().__init__(name="dummy_agent")
31+
self.sub_agents = []
32+
33+
34+
class _DummyAgentLoader:
35+
36+
def load_agent(self, app_name: str) -> BaseAgent:
37+
return _DummyAgent()
38+
39+
def list_agents(self) -> list[str]:
40+
return ["test_app"]
41+
42+
def list_agents_detailed(self) -> list[dict[str, Any]]:
43+
return []
44+
45+
46+
class _CapturingRunner:
47+
48+
def __init__(self) -> None:
49+
self.captured_run_config = None
50+
51+
async def run_live(
52+
self,
53+
*,
54+
session,
55+
live_request_queue,
56+
run_config=None,
57+
**unused_kwargs,
58+
):
59+
self.captured_run_config = run_config
60+
yield Event(author="runner")
61+
62+
63+
def test_run_live_applies_run_config_query_options():
64+
session_service = InMemorySessionService()
65+
asyncio.run(
66+
session_service.create_session(
67+
app_name="test_app",
68+
user_id="user",
69+
session_id="session",
70+
state={},
71+
)
72+
)
73+
74+
runner = _CapturingRunner()
75+
adk_web_server = AdkWebServer(
76+
agent_loader=_DummyAgentLoader(),
77+
session_service=session_service,
78+
memory_service=types.SimpleNamespace(),
79+
artifact_service=types.SimpleNamespace(),
80+
credential_service=types.SimpleNamespace(),
81+
eval_sets_manager=types.SimpleNamespace(),
82+
eval_set_results_manager=types.SimpleNamespace(),
83+
agents_dir=".",
84+
)
85+
86+
async def _get_runner_async(_self, _app_name: str):
87+
return runner
88+
89+
adk_web_server.get_runner_async = _get_runner_async.__get__(adk_web_server) # pytype: disable=attribute-error
90+
91+
fast_api_app = adk_web_server.get_fast_api_app(
92+
setup_observer=lambda _observer, _server: None,
93+
tear_down_observer=lambda _observer, _server: None,
94+
)
95+
96+
client = TestClient(fast_api_app)
97+
url = (
98+
"/run_live"
99+
"?app_name=test_app"
100+
"&user_id=user"
101+
"&session_id=session"
102+
"&modalities=TEXT"
103+
"&modalities=AUDIO"
104+
"&proactive_audio=true"
105+
"&enable_affective_dialog=true"
106+
"&enable_session_resumption=true"
107+
)
108+
109+
with client.websocket_connect(url) as ws:
110+
_ = ws.receive_text()
111+
112+
run_config = runner.captured_run_config
113+
assert run_config is not None
114+
assert run_config.response_modalities == ["TEXT", "AUDIO"]
115+
assert run_config.enable_affective_dialog is True
116+
assert run_config.proactivity is not None
117+
assert run_config.proactivity.proactive_audio is True
118+
assert run_config.session_resumption is not None
119+
assert run_config.session_resumption.transparent is True
120+
121+
122+
@pytest.mark.parametrize(
123+
(
124+
"query,expected_enable_affective,expected_proactive_audio,"
125+
"expected_session_resumption_transparent"
126+
),
127+
[
128+
("", None, None, None),
129+
("&proactive_audio=true", None, True, None),
130+
("&proactive_audio=false", None, False, None),
131+
("&enable_affective_dialog=true", True, None, None),
132+
("&enable_affective_dialog=false", False, None, None),
133+
("&enable_session_resumption=true", None, None, True),
134+
("&enable_session_resumption=false", None, None, False),
135+
],
136+
)
137+
def test_run_live_defaults_and_individual_options(
138+
query: str,
139+
expected_enable_affective: bool | None,
140+
expected_proactive_audio: bool | None,
141+
expected_session_resumption_transparent: bool | None,
142+
):
143+
session_service = InMemorySessionService()
144+
asyncio.run(
145+
session_service.create_session(
146+
app_name="test_app",
147+
user_id="user",
148+
session_id="session",
149+
state={},
150+
)
151+
)
152+
153+
runner = _CapturingRunner()
154+
adk_web_server = AdkWebServer(
155+
agent_loader=_DummyAgentLoader(),
156+
session_service=session_service,
157+
memory_service=types.SimpleNamespace(),
158+
artifact_service=types.SimpleNamespace(),
159+
credential_service=types.SimpleNamespace(),
160+
eval_sets_manager=types.SimpleNamespace(),
161+
eval_set_results_manager=types.SimpleNamespace(),
162+
agents_dir=".",
163+
)
164+
165+
async def _get_runner_async(_self, _app_name: str):
166+
return runner
167+
168+
adk_web_server.get_runner_async = _get_runner_async.__get__(adk_web_server) # pytype: disable=attribute-error
169+
170+
fast_api_app = adk_web_server.get_fast_api_app(
171+
setup_observer=lambda _observer, _server: None,
172+
tear_down_observer=lambda _observer, _server: None,
173+
)
174+
175+
client = TestClient(fast_api_app)
176+
url = (
177+
"/run_live"
178+
"?app_name=test_app"
179+
"&user_id=user"
180+
"&session_id=session"
181+
"&modalities=AUDIO"
182+
f"{query}"
183+
)
184+
185+
with client.websocket_connect(url) as ws:
186+
_ = ws.receive_text()
187+
188+
run_config = runner.captured_run_config
189+
assert run_config is not None
190+
assert run_config.enable_affective_dialog == expected_enable_affective
191+
192+
if expected_proactive_audio is None:
193+
assert run_config.proactivity is None
194+
else:
195+
assert run_config.proactivity is not None
196+
assert run_config.proactivity.proactive_audio is expected_proactive_audio
197+
198+
if expected_session_resumption_transparent is None:
199+
assert run_config.session_resumption is None
200+
else:
201+
assert run_config.session_resumption is not None
202+
assert (
203+
run_config.session_resumption.transparent
204+
is expected_session_resumption_transparent
205+
)

0 commit comments

Comments
 (0)