Merge pull request #3692 from pipecat-ai/mb/request-metadata-updates

Rename RequestMetadataFrame to ServiceSwitcherRequestMetadataFrame with service targeting
This commit is contained in:
Mark Backman
2026-02-09 18:19:29 -05:00
committed by GitHub
5 changed files with 31 additions and 22 deletions

View File

@@ -0,0 +1 @@
- Renamed `RequestMetadataFrame` to `ServiceSwitcherRequestMetadataFrame` and added a `service` field to target a specific service. The frame is now pushed downstream by services after handling instead of being silently consumed.

View File

@@ -1721,16 +1721,19 @@ class STTMetadataFrame(ServiceMetadataFrame):
@dataclass @dataclass
class RequestMetadataFrame(ControlFrame): class ServiceSwitcherRequestMetadataFrame(ControlFrame):
"""Request services to re-emit their metadata frames. """Request a service to re-emit its metadata frames.
Used by ServiceSwitcher when switching active services to ensure Used by ServiceSwitcher when switching active services to ensure
downstream processors receive updated metadata from the newly active service. downstream processors receive updated metadata from the newly active service.
Services that receive this frame should re-push their metadata frame Services that receive this frame should re-push their metadata frame
(e.g., STTMetadataFrame for STT services). (e.g., STTMetadataFrame for STT services).
Parameters:
service: The target service that should re-emit its metadata.
""" """
pass service: "FrameProcessor"
# #

View File

@@ -12,9 +12,9 @@ from typing import Any, Generic, List, Optional, Type, TypeVar
from pipecat.frames.frames import ( from pipecat.frames.frames import (
Frame, Frame,
ManuallySwitchServiceFrame, ManuallySwitchServiceFrame,
RequestMetadataFrame,
ServiceMetadataFrame, ServiceMetadataFrame,
ServiceSwitcherFrame, ServiceSwitcherFrame,
ServiceSwitcherRequestMetadataFrame,
) )
from pipecat.pipeline.parallel_pipeline import ParallelPipeline from pipecat.pipeline.parallel_pipeline import ParallelPipeline
from pipecat.processors.filters.function_filter import FunctionFilter from pipecat.processors.filters.function_filter import FunctionFilter
@@ -220,16 +220,19 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM): async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
"""Push a frame out of the service switcher. """Push a frame out of the service switcher.
Suppresses `RequestMetadataFrame` (internal to the switcher) and Suppresses `ServiceSwitcherRequestMetadataFrame` targeting the active
`ServiceMetadataFrame` from inactive services so only the active service (since it has already been handled) and `ServiceMetadataFrame`
service's metadata reaches downstream processors. One case this happens from inactive services so only the active service's metadata reaches
is with `StartFrame` since all the filters let it pass, and `StartFrame` downstream processors. One case this happens is with `StartFrame` since
causes the service to generate `ServiceMetadataFrame`. all the filters let it pass, and `StartFrame` causes the service to
generate `ServiceMetadataFrame`.
""" """
# Don't let RequestMetadataFrame out. # Consume ServiceSwitcherRequestMetadataFrame once the targeted service
if isinstance(frame, RequestMetadataFrame): # has handled it (i.e. the active service).
return if isinstance(frame, ServiceSwitcherRequestMetadataFrame):
if frame.service == self.strategy.active_service:
return
# Only let metadata from the active service escape. # Only let metadata from the active service escape.
if isinstance(frame, ServiceMetadataFrame): if isinstance(frame, ServiceMetadataFrame):
@@ -255,6 +258,6 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
# If we switched to a new service, request its metadata. # If we switched to a new service, request its metadata.
if service: if service:
await service.queue_frame(RequestMetadataFrame()) await service.queue_frame(ServiceSwitcherRequestMetadataFrame(service=service))
else: else:
await super().process_frame(frame, direction) await super().process_frame(frame, direction)

View File

@@ -21,7 +21,7 @@ from pipecat.frames.frames import (
Frame, Frame,
InterruptionFrame, InterruptionFrame,
MetricsFrame, MetricsFrame,
RequestMetadataFrame, ServiceSwitcherRequestMetadataFrame,
StartFrame, StartFrame,
STTMetadataFrame, STTMetadataFrame,
STTMuteFrame, STTMuteFrame,
@@ -264,9 +264,9 @@ class STTService(AIService):
# Push StartFrame first, then metadata so downstream receives them in order # Push StartFrame first, then metadata so downstream receives them in order
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
await self._push_stt_metadata() await self._push_stt_metadata()
elif isinstance(frame, RequestMetadataFrame): elif isinstance(frame, ServiceSwitcherRequestMetadataFrame):
# Don't push the RequestMetadataFrame, just push the metadata
await self._push_stt_metadata() await self._push_stt_metadata()
await self.push_frame(frame, direction)
elif isinstance(frame, AudioRawFrame): elif isinstance(frame, AudioRawFrame):
# In this service we accumulate audio internally and at the end we # In this service we accumulate audio internally and at the end we
# push a TextFrame. We also push audio downstream in case someone # push a TextFrame. We also push audio downstream in case someone

View File

@@ -13,8 +13,8 @@ from dataclasses import dataclass
from pipecat.frames.frames import ( from pipecat.frames.frames import (
Frame, Frame,
ManuallySwitchServiceFrame, ManuallySwitchServiceFrame,
RequestMetadataFrame,
ServiceMetadataFrame, ServiceMetadataFrame,
ServiceSwitcherRequestMetadataFrame,
StartFrame, StartFrame,
SystemFrame, SystemFrame,
TextFrame, TextFrame,
@@ -68,7 +68,7 @@ class MockMetadataFrame(ServiceMetadataFrame):
class MockMetadataService(FrameProcessor): class MockMetadataService(FrameProcessor):
"""A mock service that emits ServiceMetadataFrame like STT services. """A mock service that emits ServiceMetadataFrame like STT services.
Pushes MockMetadataFrame on StartFrame and RequestMetadataFrame. Pushes MockMetadataFrame on StartFrame and ServiceSwitcherRequestMetadataFrame.
""" """
def __init__(self, test_name: str, **kwargs): def __init__(self, test_name: str, **kwargs):
@@ -84,9 +84,9 @@ class MockMetadataService(FrameProcessor):
if isinstance(frame, StartFrame): if isinstance(frame, StartFrame):
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
await self._push_metadata() await self._push_metadata()
elif isinstance(frame, RequestMetadataFrame): elif isinstance(frame, ServiceSwitcherRequestMetadataFrame):
# Don't push RequestMetadataFrame downstream (it's internal)
await self._push_metadata() await self._push_metadata()
await self.push_frame(frame, direction)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
@@ -472,9 +472,11 @@ class TestServiceSwitcherMetadata(unittest.IsolatedAsyncioTestCase):
expected_up_frames=[], expected_up_frames=[],
) )
# service2 should have received RequestMetadataFrame after becoming active # service2 should have received ServiceSwitcherRequestMetadataFrame after becoming active
request_frames = [ request_frames = [
f for f in self.service2.processed_frames if isinstance(f, RequestMetadataFrame) f
for f in self.service2.processed_frames
if isinstance(f, ServiceSwitcherRequestMetadataFrame)
] ]
self.assertEqual(len(request_frames), 1) self.assertEqual(len(request_frames), 1)