Merge pull request #1332 from pipecat-ai/aleix/base-object-and-event-handlers

introduce BaseObject class
This commit is contained in:
Aleix Conchillo Flaqué
2025-03-05 13:41:27 -08:00
committed by GitHub
9 changed files with 103 additions and 160 deletions

View File

@@ -9,26 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support for a unified format for specifying function calling across all LLM services. - Added new base class `BaseObject` which is now the base class of
```python `FrameProcessor`, `PipelineRunner`, `PipelineTask` and `BaseTransport`. The
weather_function = FunctionSchema( new `BaseObject` adds supports for event handlers.
name="get_current_weather",
description="Get the current weather", - Added support for a unified format for specifying function calling across all
properties={ LLM services.
"location": {
"type": "string", ```python
"description": "The city and state, e.g. San Francisco, CA", weather_function = FunctionSchema(
}, name="get_current_weather",
"format": { description="Get the current weather",
"type": "string", properties={
"enum": ["celsius", "fahrenheit"], "location": {
"description": "The temperature unit to use. Infer this from the user's location.", "type": "string",
}, "description": "The city and state, e.g. San Francisco, CA",
}, },
required=["location"], "format": {
) "type": "string",
tools = ToolsSchema(standard_tools=[weather_function]) "enum": ["celsius", "fahrenheit"],
``` "description": "The temperature unit to use. Infer this from the user's location.",
},
},
required=["location"],
)
tools = ToolsSchema(standard_tools=[weather_function])
```
- Added `speech_threshold` parameter to `GladiaSTTService`. - Added `speech_threshold` parameter to `GladiaSTTService`.

View File

@@ -4,7 +4,7 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
from abc import abstractmethod from abc import ABC, abstractmethod
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
@@ -33,7 +33,7 @@ class VADParams(BaseModel):
min_volume: float = VAD_MIN_VOLUME min_volume: float = VAD_MIN_VOLUME
class VADAnalyzer: class VADAnalyzer(ABC):
def __init__(self, *, sample_rate: Optional[int] = None, params: VADParams): def __init__(self, *, sample_rate: Optional[int] = None, params: VADParams):
self._init_sample_rate = sample_rate self._init_sample_rate = sample_rate
self._sample_rate = 0 self._sample_rate = 0

View File

@@ -5,25 +5,14 @@
# #
import asyncio import asyncio
from abc import ABC, abstractmethod from abc import abstractmethod
from typing import AsyncIterable, Iterable from typing import AsyncIterable, Iterable
from pipecat.frames.frames import Frame from pipecat.frames.frames import Frame
from pipecat.utils.base_object import BaseObject
class BaseTask(ABC): class BaseTask(BaseObject):
@property
@abstractmethod
def id(self) -> int:
"""Returns the unique indetifier for this task."""
pass
@property
@abstractmethod
def name(self) -> str:
"""Returns the name of this task."""
pass
@abstractmethod @abstractmethod
def set_event_loop(self, loop: asyncio.AbstractEventLoop): def set_event_loop(self, loop: asyncio.AbstractEventLoop):
"""Sets the event loop that this task will run on.""" """Sets the event loop that this task will run on."""

View File

@@ -12,10 +12,10 @@ from typing import Optional
from loguru import logger from loguru import logger
from pipecat.pipeline.task import PipelineTask from pipecat.pipeline.task import PipelineTask
from pipecat.utils.utils import obj_count, obj_id from pipecat.utils.base_object import BaseObject
class PipelineRunner: class PipelineRunner(BaseObject):
def __init__( def __init__(
self, self,
*, *,
@@ -24,8 +24,7 @@ class PipelineRunner:
force_gc: bool = False, force_gc: bool = False,
loop: Optional[asyncio.AbstractEventLoop] = None, loop: Optional[asyncio.AbstractEventLoop] = None,
): ):
self.id: int = obj_id() super().__init__(name=name)
self.name: str = name or f"{self.__class__.__name__}#{obj_count(self)}"
self._tasks = {} self._tasks = {}
self._sig_task = None self._sig_task = None
@@ -74,6 +73,3 @@ class PipelineRunner:
collected = gc.collect() collected = gc.collect()
logger.debug(f"Garbage collector: collected {collected} objects.") logger.debug(f"Garbage collector: collected {collected} objects.")
logger.debug(f"Garbage collector: uncollectable objects {gc.garbage}") logger.debug(f"Garbage collector: uncollectable objects {gc.garbage}")
def __str__(self):
return self.name

View File

@@ -32,7 +32,6 @@ from pipecat.pipeline.base_task import BaseTask
from pipecat.pipeline.task_observer import TaskObserver from pipecat.pipeline.task_observer import TaskObserver
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.utils.asyncio import BaseTaskManager, TaskManager from pipecat.utils.asyncio import BaseTaskManager, TaskManager
from pipecat.utils.utils import obj_count, obj_id
HEARTBEAT_SECONDS = 1.0 HEARTBEAT_SECONDS = 1.0
HEARTBEAT_MONITOR_SECONDS = HEARTBEAT_SECONDS * 5 HEARTBEAT_MONITOR_SECONDS = HEARTBEAT_SECONDS * 5
@@ -138,9 +137,7 @@ class PipelineTask(BaseTask):
task_manager: Optional[BaseTaskManager] = None, task_manager: Optional[BaseTaskManager] = None,
check_dangling_tasks: bool = True, check_dangling_tasks: bool = True,
): ):
self._id: int = obj_id() super().__init__()
self._name: str = f"{self.__class__.__name__}#{obj_count(self)}"
self._pipeline = pipeline self._pipeline = pipeline
self._clock = clock self._clock = clock
self._params = params self._params = params
@@ -180,16 +177,6 @@ class PipelineTask(BaseTask):
self._observer = TaskObserver(observers=observers, task_manager=self._task_manager) self._observer = TaskObserver(observers=observers, task_manager=self._task_manager)
@property
def id(self) -> int:
"""Returns the unique indetifier for this task."""
return self._id
@property
def name(self) -> str:
"""Returns the name of this task."""
return self._name
@property @property
def params(self) -> PipelineParams: def params(self) -> PipelineParams:
"""Returns the pipeline parameters of this task.""" """Returns the pipeline parameters of this task."""
@@ -434,6 +421,3 @@ class PipelineTask(BaseTask):
tasks = [t.get_name() for t in self._task_manager.current_tasks()] tasks = [t.get_name() for t in self._task_manager.current_tasks()]
if tasks: if tasks:
logger.warning(f"Dangling tasks detected: {tasks}") logger.warning(f"Dangling tasks detected: {tasks}")
def __str__(self):
return self.name

View File

@@ -13,7 +13,6 @@ from pipecat.frames.frames import Frame
from pipecat.observers.base_observer import BaseObserver from pipecat.observers.base_observer import BaseObserver
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.utils.asyncio import BaseTaskManager from pipecat.utils.asyncio import BaseTaskManager
from pipecat.utils.utils import obj_count, obj_id
@dataclass @dataclass
@@ -56,20 +55,10 @@ class TaskObserver(BaseObserver):
""" """
def __init__(self, *, observers: List[BaseObserver] = [], task_manager: BaseTaskManager): def __init__(self, *, observers: List[BaseObserver] = [], task_manager: BaseTaskManager):
self._id: int = obj_id()
self._name: str = f"{self.__class__.__name__}#{obj_count(self)}"
self._observers = observers self._observers = observers
self._task_manager = task_manager self._task_manager = task_manager
self._proxies: List[Proxy] = [] self._proxies: List[Proxy] = []
@property
def id(self) -> int:
return self._id
@property
def name(self) -> str:
return self._name
async def start(self): async def start(self):
"""Starts all proxy observer tasks.""" """Starts all proxy observer tasks."""
self._proxies = self._create_proxies(self._observers) self._proxies = self._create_proxies(self._observers)
@@ -100,7 +89,7 @@ class TaskObserver(BaseObserver):
queue = asyncio.Queue() queue = asyncio.Queue()
task = self._task_manager.create_task( task = self._task_manager.create_task(
self._proxy_task_handler(queue, observer), self._proxy_task_handler(queue, observer),
f"{self}::{observer.__class__.__name__}::_proxy_task_handler", f"TaskObserver::{observer.__class__.__name__}::_proxy_task_handler",
) )
proxy = Proxy(queue=queue, task=task, observer=observer) proxy = Proxy(queue=queue, task=task, observer=observer)
proxies.append(proxy) proxies.append(proxy)
@@ -112,6 +101,3 @@ class TaskObserver(BaseObserver):
await observer.on_push_frame( await observer.on_push_frame(
data.src, data.dst, data.frame, data.direction, data.timestamp data.src, data.dst, data.frame, data.direction, data.timestamp
) )
def __str__(self):
return self.name

View File

@@ -5,7 +5,6 @@
# #
import asyncio import asyncio
import inspect
from enum import Enum from enum import Enum
from typing import Awaitable, Callable, Coroutine, Optional from typing import Awaitable, Callable, Coroutine, Optional
@@ -24,7 +23,7 @@ from pipecat.frames.frames import (
from pipecat.metrics.metrics import LLMTokenUsage, MetricsData from pipecat.metrics.metrics import LLMTokenUsage, MetricsData
from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics
from pipecat.utils.asyncio import BaseTaskManager from pipecat.utils.asyncio import BaseTaskManager
from pipecat.utils.utils import obj_count, obj_id from pipecat.utils.base_object import BaseObject
class FrameDirection(Enum): class FrameDirection(Enum):
@@ -32,7 +31,7 @@ class FrameDirection(Enum):
UPSTREAM = 2 UPSTREAM = 2
class FrameProcessor: class FrameProcessor(BaseObject):
def __init__( def __init__(
self, self,
*, *,
@@ -40,14 +39,11 @@ class FrameProcessor:
metrics: Optional[FrameProcessorMetrics] = None, metrics: Optional[FrameProcessorMetrics] = None,
**kwargs, **kwargs,
): ):
self._id: int = obj_id() super().__init__(name=name)
self._name = name or f"{self.__class__.__name__}#{obj_count(self)}"
self._parent: Optional["FrameProcessor"] = None self._parent: Optional["FrameProcessor"] = None
self._prev: Optional["FrameProcessor"] = None self._prev: Optional["FrameProcessor"] = None
self._next: Optional["FrameProcessor"] = None self._next: Optional["FrameProcessor"] = None
self._event_handlers: dict = {}
# Clock # Clock
self._clock: Optional[BaseClock] = None self._clock: Optional[BaseClock] = None
@@ -254,23 +250,6 @@ class FrameProcessor:
else: else:
await self.__push_queue.put((frame, direction)) await self.__push_queue.put((frame, direction))
def event_handler(self, event_name: str):
def decorator(handler):
self.add_event_handler(event_name, handler)
return handler
return decorator
def add_event_handler(self, event_name: str, handler):
if event_name not in self._event_handlers:
raise Exception(f"Event handler {event_name} not registered")
self._event_handlers[event_name].append(handler)
def _register_event_handler(self, event_name: str):
if event_name in self._event_handlers:
raise Exception(f"Event handler {event_name} already registered")
self._event_handlers[event_name] = []
async def __start(self, frame: StartFrame): async def __start(self, frame: StartFrame):
self.__create_input_task() self.__create_input_task()
self.__create_push_task() self.__create_push_task()
@@ -385,16 +364,3 @@ class FrameProcessor:
(frame, direction) = await self.__push_queue.get() (frame, direction) = await self.__push_queue.get()
await self.__internal_push_frame(frame, direction) await self.__internal_push_frame(frame, direction)
self.__push_queue.task_done() self.__push_queue.task_done()
async def _call_event_handler(self, event_name: str, *args, **kwargs):
try:
for handler in self._event_handlers[event_name]:
if inspect.iscoroutinefunction(handler):
await handler(self, *args, **kwargs)
else:
handler(self, *args, **kwargs)
except Exception as e:
logger.exception(f"Exception in event handler {event_name}: {e}")
def __str__(self):
return self.name

View File

@@ -4,18 +4,16 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
import inspect from abc import abstractmethod
from abc import ABC, abstractmethod
from typing import Optional from typing import Optional
from loguru import logger
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from pipecat.audio.filters.base_audio_filter import BaseAudioFilter from pipecat.audio.filters.base_audio_filter import BaseAudioFilter
from pipecat.audio.mixers.base_audio_mixer import BaseAudioMixer from pipecat.audio.mixers.base_audio_mixer import BaseAudioMixer
from pipecat.audio.vad.vad_analyzer import VADAnalyzer from pipecat.audio.vad.vad_analyzer import VADAnalyzer
from pipecat.processors.frame_processor import FrameProcessor from pipecat.processors.frame_processor import FrameProcessor
from pipecat.utils.utils import obj_count, obj_id from pipecat.utils.base_object import BaseObject
class TransportParams(BaseModel): class TransportParams(BaseModel):
@@ -43,7 +41,7 @@ class TransportParams(BaseModel):
vad_analyzer: Optional[VADAnalyzer] = None vad_analyzer: Optional[VADAnalyzer] = None
class BaseTransport(ABC): class BaseTransport(BaseObject):
def __init__( def __init__(
self, self,
*, *,
@@ -51,54 +49,14 @@ class BaseTransport(ABC):
input_name: Optional[str] = None, input_name: Optional[str] = None,
output_name: Optional[str] = None, output_name: Optional[str] = None,
): ):
self._id: int = obj_id() super().__init__(name=name)
self._name = name or f"{self.__class__.__name__}#{obj_count(self)}"
self._input_name = input_name self._input_name = input_name
self._output_name = output_name self._output_name = output_name
self._event_handlers: dict = {}
@property
def id(self) -> int:
return self._id
@property
def name(self) -> str:
return self._name
@abstractmethod @abstractmethod
def input(self) -> FrameProcessor: def input(self) -> FrameProcessor:
raise NotImplementedError pass
@abstractmethod @abstractmethod
def output(self) -> FrameProcessor: def output(self) -> FrameProcessor:
raise NotImplementedError pass
def event_handler(self, event_name: str):
def decorator(handler):
self.add_event_handler(event_name, handler)
return handler
return decorator
def add_event_handler(self, event_name: str, handler):
if event_name not in self._event_handlers:
raise Exception(f"Event handler {event_name} not registered")
self._event_handlers[event_name].append(handler)
def _register_event_handler(self, event_name: str):
if event_name in self._event_handlers:
raise Exception(f"Event handler {event_name} already registered")
self._event_handlers[event_name] = []
async def _call_event_handler(self, event_name: str, *args, **kwargs):
try:
for handler in self._event_handlers[event_name]:
if inspect.iscoroutinefunction(handler):
await handler(self, *args, **kwargs)
else:
handler(self, *args, **kwargs)
except Exception as e:
logger.exception(f"Exception in event handler {event_name}: {e}")
def __str__(self):
return self.name

View File

@@ -0,0 +1,58 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import inspect
from abc import ABC
from typing import Optional
from loguru import logger
from pipecat.utils.utils import obj_count, obj_id
class BaseObject(ABC):
def __init__(self, *, name: Optional[str] = None):
self._id: int = obj_id()
self._name = name or f"{self.__class__.__name__}#{obj_count(self)}"
self._event_handlers: dict = {}
@property
def id(self) -> int:
return self._id
@property
def name(self) -> str:
return self._name
def event_handler(self, event_name: str):
def decorator(handler):
self.add_event_handler(event_name, handler)
return handler
return decorator
def add_event_handler(self, event_name: str, handler):
if event_name not in self._event_handlers:
raise Exception(f"Event handler {event_name} not registered")
self._event_handlers[event_name].append(handler)
def _register_event_handler(self, event_name: str):
if event_name in self._event_handlers:
raise Exception(f"Event handler {event_name} already registered")
self._event_handlers[event_name] = []
async def _call_event_handler(self, event_name: str, *args, **kwargs):
try:
for handler in self._event_handlers[event_name]:
if inspect.iscoroutinefunction(handler):
await handler(self, *args, **kwargs)
else:
handler(self, *args, **kwargs)
except Exception as e:
logger.exception(f"Exception in event handler {event_name}: {e}")
def __str__(self):
return self.name