Skip to content

Commit 198a328

Browse files
DeanChensjcopybara-github
authored andcommitted
chore(live): move audio and transcription managers into google.adk.live
Migrate `AudioCacheManager`, `AudioCacheConfig`, `TranscriptionManager`, and `AudioTranscriber` from `google.adk.flows.llm_flows` to `google.adk.live`. Backward-compatible re-exports are maintained in `google.adk.flows.llm_flows` for all moved symbols. `RealtimeCacheEntry` also moves, from `google.adk.agents.invocation_context` to `google.adk.live._audio_cache_manager`, because the cache manager is its only real owner. `invocation_context` re-exports it, so that import path keeps working too. This is a pure move: the moved code, including `RealtimeCacheEntry`'s pydantic config, is unchanged, and nothing is added to `google.adk.live.__all__` -- the moved symbols are reached through their module path, so the move commits to no new public API. `google.adk.live` does not import from `google.adk.flows.llm_flows`, so the two modules that needed `require_agent_name` carry a private copy rather than importing upward. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 972094217
1 parent e1ae543 commit 198a328

15 files changed

Lines changed: 1493 additions & 1272 deletions

src/google/adk/agents/invocation_context.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from ..auth.credential_service.base_credential_service import BaseCredentialService
3333
from ..events._branch_path import _BranchPath
3434
from ..events.event import Event
35+
from ..live._audio_cache_manager import RealtimeCacheEntry as RealtimeCacheEntry
3536
from ..live.live_request_queue import LiveRequestQueue
3637
from ..memory.base_memory_service import BaseMemoryService
3738
from ..plugins.plugin_manager import PluginManager
@@ -53,25 +54,6 @@ class LlmCallsLimitExceededError(Exception):
5354
"""Error thrown when the number of LLM calls exceed the limit."""
5455

5556

56-
class RealtimeCacheEntry(BaseModel):
57-
"""Store audio data chunks for caching before flushing."""
58-
59-
model_config = ConfigDict(
60-
arbitrary_types_allowed=True,
61-
extra="forbid",
62-
)
63-
"""The pydantic model config."""
64-
65-
role: str
66-
"""The role that created this audio data, typically "user" or "model"."""
67-
68-
data: types.Blob
69-
"""The audio data chunk."""
70-
71-
timestamp: float
72-
"""Timestamp when the audio chunk was received."""
73-
74-
7557
class _InvocationCostManager(BaseModel):
7658
"""A container to keep track of the cost of invocation.
7759

src/google/adk/flows/llm_flows/audio_cache_manager.py

Lines changed: 9 additions & 260 deletions
Original file line numberDiff line numberDiff line change
@@ -12,269 +12,18 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from __future__ import annotations
15+
"""Backward compatibility module for AudioCacheManager.
1616
17-
import logging
18-
from typing import TYPE_CHECKING
17+
AudioCacheManager and AudioCacheConfig have been moved to
18+
``google.adk.live.audio_cache_manager``.
19+
"""
1920

20-
from google.adk.platform import time as platform_time
21-
from google.genai import types
21+
from __future__ import annotations
2222

23-
from ...agents.invocation_context import RealtimeCacheEntry
24-
from ...events.event import Event
25-
from ._invocation_utils import require_agent_name
23+
import logging
2624

27-
if TYPE_CHECKING:
28-
from ...agents.invocation_context import InvocationContext
25+
from ...live._audio_cache_manager import AudioCacheConfig as AudioCacheConfig
26+
from ...live._audio_cache_manager import AudioCacheManager as AudioCacheManager
27+
from ...live._audio_cache_manager import RealtimeCacheEntry as RealtimeCacheEntry
2928

3029
logger = logging.getLogger('google_adk.' + __name__)
31-
32-
33-
def _require_audio_data(blob: types.Blob) -> bytes:
34-
data = blob.data
35-
if not isinstance(data, bytes):
36-
raise ValueError('Audio blobs must contain byte data.')
37-
return data
38-
39-
40-
class AudioCacheManager:
41-
"""Manages audio caching and flushing for live streaming flows."""
42-
43-
def __init__(self, config: AudioCacheConfig | None = None) -> None:
44-
"""Initialize the audio cache manager.
45-
46-
Args:
47-
config: Configuration for audio caching behavior.
48-
"""
49-
self.config = config or AudioCacheConfig()
50-
51-
def cache_audio(
52-
self,
53-
invocation_context: InvocationContext,
54-
audio_blob: types.Blob,
55-
cache_type: str,
56-
) -> None:
57-
"""Cache incoming user or outgoing model audio data.
58-
59-
Args:
60-
invocation_context: The current invocation context.
61-
audio_blob: The audio data to cache.
62-
cache_type: Type of audio to cache, either 'input' or 'output'.
63-
64-
Raises:
65-
ValueError: If cache_type is not 'input' or 'output'.
66-
"""
67-
audio_data = _require_audio_data(audio_blob)
68-
if cache_type == 'input':
69-
if not invocation_context.input_realtime_cache:
70-
invocation_context.input_realtime_cache = []
71-
cache = invocation_context.input_realtime_cache
72-
role = 'user'
73-
elif cache_type == 'output':
74-
if not invocation_context.output_realtime_cache:
75-
invocation_context.output_realtime_cache = []
76-
cache = invocation_context.output_realtime_cache
77-
role = 'model'
78-
else:
79-
raise ValueError("cache_type must be either 'input' or 'output'")
80-
81-
audio_entry = RealtimeCacheEntry(
82-
role=role, data=audio_blob, timestamp=platform_time.get_time()
83-
)
84-
cache.append(audio_entry)
85-
86-
logger.debug(
87-
'Cached %s audio chunk: %d bytes, cache size: %d',
88-
cache_type,
89-
len(audio_data),
90-
len(cache),
91-
)
92-
93-
async def flush_caches(
94-
self,
95-
invocation_context: InvocationContext,
96-
flush_user_audio: bool = True,
97-
flush_model_audio: bool = True,
98-
) -> list[Event]:
99-
"""Flush audio caches to artifact services.
100-
101-
The multimodality data is saved in artifact service in the format of
102-
audio file. The file data reference is added to the session as an event.
103-
The audio file follows the naming convention: artifact_ref =
104-
f"artifact://{invocation_context.app_name}/{invocation_context.user_id}/
105-
{invocation_context.session.id}/_adk_live/{filename}#{revision_id}"
106-
107-
Note: video data is not supported yet.
108-
109-
Args:
110-
invocation_context: The invocation context containing audio caches.
111-
flush_user_audio: Whether to flush the input (user) audio cache.
112-
flush_model_audio: Whether to flush the output (model) audio cache.
113-
114-
Returns:
115-
A list of Event objects created from the flushed caches.
116-
"""
117-
flushed_events: list[Event] = []
118-
if flush_user_audio and invocation_context.input_realtime_cache:
119-
audio_event = await self._flush_cache_to_services(
120-
invocation_context,
121-
invocation_context.input_realtime_cache,
122-
'input_audio',
123-
)
124-
if audio_event:
125-
flushed_events.append(audio_event)
126-
invocation_context.input_realtime_cache = []
127-
128-
if flush_model_audio and invocation_context.output_realtime_cache:
129-
logger.debug('Flushed output audio cache')
130-
audio_event = await self._flush_cache_to_services(
131-
invocation_context,
132-
invocation_context.output_realtime_cache,
133-
'output_audio',
134-
)
135-
if audio_event:
136-
flushed_events.append(audio_event)
137-
invocation_context.output_realtime_cache = []
138-
139-
return flushed_events
140-
141-
async def _flush_cache_to_services(
142-
self,
143-
invocation_context: InvocationContext,
144-
audio_cache: list[RealtimeCacheEntry],
145-
cache_type: str,
146-
) -> Event | None:
147-
"""Flush a list of audio cache entries to artifact services.
148-
149-
The artifact service stores the actual blob. The session stores the
150-
reference to the stored blob.
151-
152-
Args:
153-
invocation_context: The invocation context.
154-
audio_cache: The audio cache to flush.
155-
cache_type: Type identifier for the cache ('input_audio' or 'output_audio').
156-
157-
Returns:
158-
The created Event if the cache was successfully flushed, None otherwise.
159-
"""
160-
if not invocation_context.artifact_service or not audio_cache:
161-
logger.debug('Skipping cache flush: no artifact service or empty cache')
162-
return None
163-
164-
try:
165-
# Combine audio chunks into a single file. Use join rather than repeated
166-
# `+=`, which is O(n^2) over the total audio size.
167-
mime_type = audio_cache[0].data.mime_type or 'audio/pcm'
168-
combined_audio_data = b''.join(
169-
entry.data.data or b'' for entry in audio_cache
170-
)
171-
172-
# Generate filename with timestamp from first audio chunk (when recording started)
173-
timestamp = int(audio_cache[0].timestamp * 1000) # milliseconds
174-
filename = f"adk_live_audio_storage_{cache_type}_{timestamp}.{mime_type.split('/')[-1]}"
175-
176-
# Save to artifact service
177-
combined_audio_part = types.Part(
178-
inline_data=types.Blob(data=combined_audio_data, mime_type=mime_type)
179-
)
180-
181-
revision_id = await invocation_context.artifact_service.save_artifact(
182-
app_name=invocation_context.app_name,
183-
user_id=invocation_context.user_id,
184-
session_id=invocation_context.session.id,
185-
filename=filename,
186-
artifact=combined_audio_part,
187-
)
188-
189-
# Create artifact reference for session service
190-
artifact_ref = f'artifact://{invocation_context.app_name}/{invocation_context.user_id}/{invocation_context.session.id}/_adk_live/{filename}#{revision_id}'
191-
192-
# Create event with file data reference to add to session
193-
# For model events, author should be the agent name, not the role
194-
author = (
195-
require_agent_name(invocation_context)
196-
if audio_cache[0].role == 'model'
197-
else audio_cache[0].role
198-
)
199-
audio_event = Event(
200-
id=Event.new_id(),
201-
invocation_id=invocation_context.invocation_id,
202-
author=author,
203-
content=types.Content(
204-
role=audio_cache[0].role,
205-
parts=[
206-
types.Part(
207-
file_data=types.FileData(
208-
file_uri=artifact_ref, mime_type=mime_type
209-
)
210-
)
211-
],
212-
),
213-
timestamp=audio_cache[0].timestamp,
214-
)
215-
216-
logger.debug(
217-
'Successfully flushed %s cache: %d chunks, %d bytes, saved as %s',
218-
cache_type,
219-
len(audio_cache),
220-
len(combined_audio_data),
221-
filename,
222-
)
223-
return audio_event
224-
225-
except Exception as e:
226-
logger.error('Failed to flush %s cache: %s', cache_type, e)
227-
return None
228-
229-
def get_cache_stats(
230-
self, invocation_context: InvocationContext
231-
) -> dict[str, int]:
232-
"""Get statistics about current cache state.
233-
234-
Args:
235-
invocation_context: The invocation context.
236-
237-
Returns:
238-
Dictionary containing cache statistics.
239-
"""
240-
input_count = len(invocation_context.input_realtime_cache or [])
241-
output_count = len(invocation_context.output_realtime_cache or [])
242-
243-
input_bytes = sum(
244-
len(_require_audio_data(entry.data))
245-
for entry in invocation_context.input_realtime_cache or []
246-
)
247-
output_bytes = sum(
248-
len(_require_audio_data(entry.data))
249-
for entry in invocation_context.output_realtime_cache or []
250-
)
251-
252-
return {
253-
'input_chunks': input_count,
254-
'output_chunks': output_count,
255-
'input_bytes': input_bytes,
256-
'output_bytes': output_bytes,
257-
'total_chunks': input_count + output_count,
258-
'total_bytes': input_bytes + output_bytes,
259-
}
260-
261-
262-
class AudioCacheConfig:
263-
"""Configuration for audio caching behavior."""
264-
265-
def __init__(
266-
self,
267-
max_cache_size_bytes: int = 10 * 1024 * 1024, # 10MB
268-
max_cache_duration_seconds: float = 300.0, # 5 minutes
269-
auto_flush_threshold: int = 100, # Number of chunks
270-
) -> None:
271-
"""Initialize audio cache configuration.
272-
273-
Args:
274-
max_cache_size_bytes: Maximum cache size in bytes before auto-flush.
275-
max_cache_duration_seconds: Maximum duration to keep data in cache.
276-
auto_flush_threshold: Number of chunks that triggers auto-flush.
277-
"""
278-
self.max_cache_size_bytes = max_cache_size_bytes
279-
self.max_cache_duration_seconds = max_cache_duration_seconds
280-
self.auto_flush_threshold = auto_flush_threshold

0 commit comments

Comments
 (0)