Consolidate service UpdateSettingsFrame into a single ServiceUpdateSettingsFrame

This commit is contained in:
Mark Backman
2024-10-01 10:35:59 -04:00
parent a397b859fe
commit 88cca7bf68
7 changed files with 106 additions and 151 deletions

View File

@@ -86,8 +86,8 @@ async def on_connected(processor):
### Changed ### Changed
- Updated individual update settings frame classes into a single UpdateSettingsFrame - Updated individual update settings frame classes into a single
class for STT, LLM, and TTS. ServiceUpdateSettingsFrame class.
- We now distinguish between input and output audio and image frames. We - We now distinguish between input and output audio and image frames. We
introduce `InputAudioRawFrame`, `OutputAudioRawFrame`, `InputImageRawFrame` introduce `InputAudioRawFrame`, `OutputAudioRawFrame`, `InputImageRawFrame`

View File

@@ -5,7 +5,7 @@
# #
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple
from pipecat.clocks.base_clock import BaseClock from pipecat.clocks.base_clock import BaseClock
from pipecat.metrics.metrics import MetricsData from pipecat.metrics.metrics import MetricsData
@@ -527,47 +527,11 @@ class UserImageRequestFrame(ControlFrame):
@dataclass @dataclass
class LLMUpdateSettingsFrame(ControlFrame): class ServiceUpdateSettingsFrame(ControlFrame):
"""A control frame containing a request to update LLM settings.""" """A control frame containing a request to update service settings."""
model: Optional[str] = None service_type: str
temperature: Optional[float] = None settings: Dict[str, Any]
top_k: Optional[int] = None
top_p: Optional[float] = None
frequency_penalty: Optional[float] = None
presence_penalty: Optional[float] = None
max_tokens: Optional[int] = None
seed: Optional[int] = None
extra: dict = field(default_factory=dict)
@dataclass
class TTSUpdateSettingsFrame(ControlFrame):
"""A control frame containing a request to update TTS settings."""
model: Optional[str] = None
voice: Optional[str] = None
language: Optional[Language] = None
speed: Optional[Union[str, float]] = None
emotion: Optional[List[str]] = None
engine: Optional[str] = None
pitch: Optional[str] = None
rate: Optional[str] = None
volume: Optional[str] = None
emphasis: Optional[str] = None
style: Optional[str] = None
style_degree: Optional[str] = None
role: Optional[str] = None
gender: Optional[str] = None
google_style: Optional[str] = None
@dataclass
class STTUpdateSettingsFrame(ControlFrame):
"""A control frame containing a request to update STT settings."""
model: Optional[str] = None
language: Optional[Language] = None
@dataclass @dataclass

View File

@@ -8,7 +8,7 @@ import asyncio
import io import io
import wave import wave
from abc import abstractmethod from abc import abstractmethod
from typing import AsyncGenerator, List, Optional, Tuple, Union from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
from loguru import logger from loguru import logger
@@ -19,15 +19,14 @@ from pipecat.frames.frames import (
ErrorFrame, ErrorFrame,
Frame, Frame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
ServiceUpdateSettingsFrame,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
STTUpdateSettingsFrame,
TextFrame, TextFrame,
TTSAudioRawFrame, TTSAudioRawFrame,
TTSSpeakFrame, TTSSpeakFrame,
TTSStartedFrame, TTSStartedFrame,
TTSStoppedFrame, TTSStoppedFrame,
TTSUpdateSettingsFrame,
UserImageRequestFrame, UserImageRequestFrame,
VisionImageRawFrame, VisionImageRawFrame,
) )
@@ -169,6 +168,7 @@ class TTSService(AIService):
self._push_stop_frames: bool = push_stop_frames self._push_stop_frames: bool = push_stop_frames
self._stop_frame_timeout_s: float = stop_frame_timeout_s self._stop_frame_timeout_s: float = stop_frame_timeout_s
self._sample_rate: int = sample_rate self._sample_rate: int = sample_rate
self._settings: Dict[str, Any] = {}
self._stop_frame_task: Optional[asyncio.Task] = None self._stop_frame_task: Optional[asyncio.Task] = None
self._stop_frame_queue: asyncio.Queue = asyncio.Queue() self._stop_frame_queue: asyncio.Queue = asyncio.Queue()
@@ -231,10 +231,6 @@ class TTSService(AIService):
async def set_role(self, role: str): async def set_role(self, role: str):
pass pass
@abstractmethod
async def flush_audio(self):
pass
@abstractmethod @abstractmethod
async def set_gender(self, gender: str): async def set_gender(self, gender: str):
pass pass
@@ -243,6 +239,10 @@ class TTSService(AIService):
async def set_google_style(self, google_style: str): async def set_google_style(self, google_style: str):
pass pass
@abstractmethod
async def flush_audio(self):
pass
# Converts the text to audio. # Converts the text to audio.
@abstractmethod @abstractmethod
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
@@ -267,6 +267,22 @@ class TTSService(AIService):
await self._stop_frame_task await self._stop_frame_task
self._stop_frame_task = None self._stop_frame_task = None
async def _update_settings(self, settings: Dict[str, Any]):
for key, value in settings.items():
setter = getattr(self, f"set_{key}", None)
if setter and callable(setter):
try:
if key == "language":
await setter(Language(value))
else:
await setter(value)
except Exception as e:
logger.warning(f"Error setting {key}: {e}")
else:
logger.warning(f"Unknown setting for TTS service: {key}")
self._settings.update(settings)
async def say(self, text: str): async def say(self, text: str):
aggregate_sentences = self._aggregate_sentences aggregate_sentences = self._aggregate_sentences
self._aggregate_sentences = False self._aggregate_sentences = False
@@ -293,8 +309,8 @@ class TTSService(AIService):
elif isinstance(frame, TTSSpeakFrame): elif isinstance(frame, TTSSpeakFrame):
await self._push_tts_frames(frame.text) await self._push_tts_frames(frame.text)
await self.flush_audio() await self.flush_audio()
elif isinstance(frame, TTSUpdateSettingsFrame): elif isinstance(frame, ServiceUpdateSettingsFrame) and frame.service_type == "tts":
await self._update_tts_settings(frame) await self._update_settings(frame.settings)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
@@ -341,34 +357,6 @@ class TTSService(AIService):
# interrupted, the text is not added to the assistant context. # interrupted, the text is not added to the assistant context.
await self.push_frame(TextFrame(text)) await self.push_frame(TextFrame(text))
async def _update_tts_settings(self, frame: TTSUpdateSettingsFrame):
if frame.model is not None:
await self.set_model(frame.model)
if frame.voice is not None:
await self.set_voice(frame.voice)
if frame.language is not None:
await self.set_language(frame.language)
if frame.speed is not None:
await self.set_speed(frame.speed)
if frame.emotion is not None:
await self.set_emotion(frame.emotion)
if frame.engine is not None:
await self.set_engine(frame.engine)
if frame.pitch is not None:
await self.set_pitch(frame.pitch)
if frame.rate is not None:
await self.set_rate(frame.rate)
if frame.volume is not None:
await self.set_volume(frame.volume)
if frame.emphasis is not None:
await self.set_emphasis(frame.emphasis)
if frame.style is not None:
await self.set_style(frame.style)
if frame.style_degree is not None:
await self.set_style_degree(frame.style_degree)
if frame.role is not None:
await self.set_role(frame.role)
async def _stop_frame_handler(self): async def _stop_frame_handler(self):
try: try:
has_started = False has_started = False
@@ -454,6 +442,7 @@ class STTService(AIService):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._settings: Dict[str, Any] = {}
@abstractmethod @abstractmethod
async def set_model(self, model: str): async def set_model(self, model: str):
@@ -468,11 +457,21 @@ class STTService(AIService):
"""Returns transcript as a string""" """Returns transcript as a string"""
pass pass
async def _update_stt_settings(self, frame: STTUpdateSettingsFrame): async def _update_settings(self, settings: Dict[str, Any]):
if frame.model is not None: for key, value in settings.items():
await self.set_model(frame.model) setter = getattr(self, f"set_{key}", None)
if frame.language is not None: if setter and callable(setter):
await self.set_language(frame.language) try:
if key == "language":
await setter(Language(value))
else:
await setter(value)
except Exception as e:
logger.warning(f"Error setting {key}: {e}")
else:
logger.warning(f"Unknown setting for STT service: {key}")
self._settings.update(settings)
async def process_audio_frame(self, frame: AudioRawFrame): async def process_audio_frame(self, frame: AudioRawFrame):
await self.process_generator(self.run_stt(frame.audio)) await self.process_generator(self.run_stt(frame.audio))
@@ -485,8 +484,8 @@ class STTService(AIService):
# 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 don't really want to push audio frames down. # push a TextFrame. We don't really want to push audio frames down.
await self.process_audio_frame(frame) await self.process_audio_frame(frame)
elif isinstance(frame, STTUpdateSettingsFrame): elif isinstance(frame, ServiceUpdateSettingsFrame) and frame.service_type == "stt":
await self._update_stt_settings(frame) await self._update_settings(frame.settings)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)

View File

@@ -25,7 +25,7 @@ from pipecat.frames.frames import (
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMUpdateSettingsFrame, ServiceUpdateSettingsFrame,
StartInterruptionFrame, StartInterruptionFrame,
TextFrame, TextFrame,
UserImageRawFrame, UserImageRawFrame,
@@ -284,20 +284,16 @@ class AnthropicLLMService(LLMService):
cache_read_input_tokens=cache_read_input_tokens, cache_read_input_tokens=cache_read_input_tokens,
) )
async def _update_settings(self, frame: LLMUpdateSettingsFrame): async def _update_settings(self, settings: Dict[str, Any]):
if frame.model is not None: for key, value in settings.items():
logger.debug(f"Switching LLM model to: [{frame.model}]") setter = getattr(self, f"set_{key}", None)
self.set_model_name(frame.model) if setter and callable(setter):
if frame.max_tokens is not None: try:
await self.set_max_tokens(frame.max_tokens) await setter(value)
if frame.temperature is not None: except Exception as e:
await self.set_temperature(frame.temperature) logger.warning(f"Error setting {key}: {e}")
if frame.top_k is not None: else:
await self.set_top_k(frame.top_k) logger.warning(f"Unknown setting for Anthropic LLM service: {key}")
if frame.top_p is not None:
await self.set_top_p(frame.top_p)
if frame.extra:
await self.set_extra(frame.extra)
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction) await super().process_frame(frame, direction)
@@ -313,8 +309,8 @@ class AnthropicLLMService(LLMService):
# UserImageRawFrames coming through the pipeline and add them # UserImageRawFrames coming through the pipeline and add them
# to the context. # to the context.
context = AnthropicLLMContext.from_image_frame(frame) context = AnthropicLLMContext.from_image_frame(frame)
elif isinstance(frame, LLMUpdateSettingsFrame): elif isinstance(frame, ServiceUpdateSettingsFrame) and frame.service_type == "llm":
await self._update_settings(frame) await self._update_settings(frame.settings)
elif isinstance(frame, LLMEnablePromptCachingFrame): elif isinstance(frame, LLMEnablePromptCachingFrame):
logger.debug(f"Setting enable prompt caching to: [{frame.enable}]") logger.debug(f"Setting enable prompt caching to: [{frame.enable}]")
self._enable_prompt_caching_beta = frame.enable self._enable_prompt_caching_beta = frame.enable

View File

@@ -6,7 +6,7 @@
import asyncio import asyncio
import json import json
from typing import AsyncGenerator, List, Literal, Optional from typing import Any, AsyncGenerator, Dict, List, Literal, Optional
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -17,7 +17,7 @@ from pipecat.frames.frames import (
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMUpdateSettingsFrame, ServiceUpdateSettingsFrame,
TextFrame, TextFrame,
TTSAudioRawFrame, TTSAudioRawFrame,
TTSStartedFrame, TTSStartedFrame,
@@ -64,6 +64,21 @@ class GoogleLLMService(LLMService):
self.set_model_name(model) self.set_model_name(model)
self._client = gai.GenerativeModel(model) self._client = gai.GenerativeModel(model)
async def set_model(self, model: str):
logger.debug(f"Switching LLM model to: [{model}]")
self._create_client(model)
async def _update_settings(self, settings: Dict[str, Any]):
for key, value in settings.items():
setter = getattr(self, f"set_{key}", None)
if setter and callable(setter):
try:
await setter(value)
except Exception as e:
logger.warning(f"Error setting {key}: {e}")
else:
logger.warning(f"Unknown setting for Google LLM service: {key}")
def _get_messages_from_openai_context(self, context: OpenAILLMContext) -> List[glm.Content]: def _get_messages_from_openai_context(self, context: OpenAILLMContext) -> List[glm.Content]:
openai_messages = context.get_messages() openai_messages = context.get_messages()
google_messages = [] google_messages = []
@@ -136,10 +151,8 @@ class GoogleLLMService(LLMService):
context = OpenAILLMContext.from_messages(frame.messages) context = OpenAILLMContext.from_messages(frame.messages)
elif isinstance(frame, VisionImageRawFrame): elif isinstance(frame, VisionImageRawFrame):
context = OpenAILLMContext.from_image_frame(frame) context = OpenAILLMContext.from_image_frame(frame)
elif isinstance(frame, LLMUpdateSettingsFrame): elif isinstance(frame, ServiceUpdateSettingsFrame) and frame.service_type == "llm":
if frame.model is not None: await self._update_settings(frame.settings)
logger.debug(f"Switching LLM model to: [{frame.model}]")
self.set_model_name(frame.model)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)

View File

@@ -24,7 +24,7 @@ from pipecat.frames.frames import (
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMUpdateSettingsFrame, ServiceUpdateSettingsFrame,
StartInterruptionFrame, StartInterruptionFrame,
TextFrame, TextFrame,
TTSAudioRawFrame, TTSAudioRawFrame,
@@ -295,22 +295,16 @@ class BaseOpenAILLMService(LLMService):
f"The LLM tried to call a function named '{function_name}', but there isn't a callback registered for that function." f"The LLM tried to call a function named '{function_name}', but there isn't a callback registered for that function."
) )
async def _update_settings(self, frame: LLMUpdateSettingsFrame): async def _update_settings(self, settings: Dict[str, Any]):
if frame.model is not None: for key, value in settings.items():
logger.debug(f"Switching LLM model to: [{frame.model}]") setter = getattr(self, f"set_{key}", None)
self.set_model_name(frame.model) if setter and callable(setter):
if frame.frequency_penalty is not None: try:
await self.set_frequency_penalty(frame.frequency_penalty) await setter(value)
if frame.presence_penalty is not None: except Exception as e:
await self.set_presence_penalty(frame.presence_penalty) logger.warning(f"Error setting {key}: {e}")
if frame.seed is not None: else:
await self.set_seed(frame.seed) logger.warning(f"Unknown setting for OpenAI LLM service: {key}")
if frame.temperature is not None:
await self.set_temperature(frame.temperature)
if frame.top_p is not None:
await self.set_top_p(frame.top_p)
if frame.extra:
await self.set_extra(frame.extra)
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction) await super().process_frame(frame, direction)
@@ -322,8 +316,8 @@ class BaseOpenAILLMService(LLMService):
context = OpenAILLMContext.from_messages(frame.messages) context = OpenAILLMContext.from_messages(frame.messages)
elif isinstance(frame, VisionImageRawFrame): elif isinstance(frame, VisionImageRawFrame):
context = OpenAILLMContext.from_image_frame(frame) context = OpenAILLMContext.from_image_frame(frame)
elif isinstance(frame, LLMUpdateSettingsFrame): elif isinstance(frame, ServiceUpdateSettingsFrame) and frame.service_type == "llm":
await self._update_settings(frame) await self._update_settings(frame.settings)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)

View File

@@ -5,16 +5,13 @@
# #
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import httpx import httpx
from loguru import logger from loguru import logger
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pipecat.frames.frames import (
LLMUpdateSettingsFrame,
)
from pipecat.services.openai import OpenAILLMService from pipecat.services.openai import OpenAILLMService
try: try:
# Together.ai is recommending OpenAI-compatible function calling, so we've switched over # Together.ai is recommending OpenAI-compatible function calling, so we've switched over
# to using the OpenAI client library here rather than the Together Python client library. # to using the OpenAI client library here rather than the Together Python client library.
@@ -104,21 +101,13 @@ class TogetherLLMService(OpenAILLMService):
logger.debug(f"Switching LLM extra to: [{extra}]") logger.debug(f"Switching LLM extra to: [{extra}]")
self._extra = extra self._extra = extra
async def _update_settings(self, frame: LLMUpdateSettingsFrame): async def _update_settings(self, settings: Dict[str, Any]):
if frame.model is not None: for key, value in settings.items():
logger.debug(f"Switching LLM model to: [{frame.model}]") setter = getattr(self, f"set_{key}", None)
self.set_model_name(frame.model) if setter and callable(setter):
if frame.frequency_penalty is not None: try:
await self.set_frequency_penalty(frame.frequency_penalty) await setter(value)
if frame.max_tokens is not None: except Exception as e:
await self.set_max_tokens(frame.max_tokens) logger.warning(f"Error setting {key}: {e}")
if frame.presence_penalty is not None: else:
await self.set_presence_penalty(frame.presence_penalty) logger.warning(f"Unknown setting for Together LLM service: {key}")
if frame.temperature is not None:
await self.set_temperature(frame.temperature)
if frame.top_k is not None:
await self.set_top_k(frame.top_k)
if frame.top_p is not None:
await self.set_top_p(frame.top_p)
if frame.extra:
await self.set_extra(frame.extra)