Consolidate update frames classes into a single UpdateSettingsFrame class

This commit is contained in:
Mark Backman
2024-09-27 10:43:49 -04:00
parent 97cba92fa5
commit 44a349386c
7 changed files with 159 additions and 195 deletions

View File

@@ -93,6 +93,9 @@ async def on_connected(processor):
### Changed ### Changed
- Updated individual update settings frame classes into a single UpdateSettingsFrame
class for STT, LLM, and TTS.
- 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`
and `OutputImageRawFrame` (and other subclasses of those). The input frames and `OutputImageRawFrame` (and other subclasses of those). The input frames

View File

@@ -4,9 +4,8 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
from typing import Any, List, Optional, Tuple
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, 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
@@ -528,113 +527,35 @@ class UserImageRequestFrame(ControlFrame):
@dataclass @dataclass
class LLMModelUpdateFrame(ControlFrame): class LLMUpdateSettingsFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM model.""" """A control frame containing a request to update LLM settings."""
model: str model: Optional[str] = None
temperature: Optional[float] = None
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 @dataclass
class LLMTemperatureUpdateFrame(ControlFrame): class TTSUpdateSettingsFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM temperature.""" """A control frame containing a request to update TTS settings."""
temperature: float model: Optional[str] = None
voice: Optional[str] = None
language: Optional[Language] = None
@dataclass @dataclass
class LLMTopKUpdateFrame(ControlFrame): class STTUpdateSettingsFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM top_k.""" """A control frame containing a request to update STT settings."""
top_k: int model: Optional[str] = None
language: Optional[Language] = None
@dataclass
class LLMTopPUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM top_p."""
top_p: float
@dataclass
class LLMFrequencyPenaltyUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM frequency
penalty.
"""
frequency_penalty: float
@dataclass
class LLMPresencePenaltyUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM presence
penalty.
"""
presence_penalty: float
@dataclass
class LLMMaxTokensUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM max tokens."""
max_tokens: int
@dataclass
class LLMSeedUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM seed."""
seed: int
@dataclass
class LLMExtraUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new LLM extra params."""
extra: dict
@dataclass
class TTSModelUpdateFrame(ControlFrame):
"""A control frame containing a request to update the TTS model."""
model: str
@dataclass
class TTSVoiceUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new TTS voice."""
voice: str
@dataclass
class TTSLanguageUpdateFrame(ControlFrame):
"""A control frame containing a request to update to a new TTS language and
optional voice.
"""
language: Language
@dataclass
class STTModelUpdateFrame(ControlFrame):
"""A control frame containing a request to update the STT model and optional
language.
"""
model: str
@dataclass
class STTLanguageUpdateFrame(ControlFrame):
"""A control frame containing a request to update to STT language."""
language: Language
@dataclass @dataclass

View File

@@ -7,10 +7,11 @@
import asyncio 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 from typing import AsyncGenerator, List, Optional, Tuple
from loguru import logger
from pipecat.frames.frames import ( from pipecat.frames.frames import (
AudioRawFrame, AudioRawFrame,
CancelFrame, CancelFrame,
@@ -18,31 +19,26 @@ from pipecat.frames.frames import (
ErrorFrame, ErrorFrame,
Frame, Frame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
STTLanguageUpdateFrame,
STTModelUpdateFrame,
StartFrame, StartFrame,
StartInterruptionFrame, StartInterruptionFrame,
STTUpdateSettingsFrame,
TextFrame,
TTSAudioRawFrame, TTSAudioRawFrame,
TTSLanguageUpdateFrame,
TTSModelUpdateFrame,
TTSSpeakFrame, TTSSpeakFrame,
TTSStartedFrame, TTSStartedFrame,
TTSStoppedFrame, TTSStoppedFrame,
TTSVoiceUpdateFrame, TTSUpdateSettingsFrame,
TextFrame,
UserImageRequestFrame, UserImageRequestFrame,
VisionImageRawFrame, VisionImageRawFrame,
) )
from pipecat.metrics.metrics import MetricsData from pipecat.metrics.metrics import MetricsData
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.audio import calculate_audio_volume from pipecat.utils.audio import calculate_audio_volume
from pipecat.utils.string import match_endofsentence from pipecat.utils.string import match_endofsentence
from pipecat.utils.time import seconds_to_nanoseconds from pipecat.utils.time import seconds_to_nanoseconds
from pipecat.utils.utils import exp_smoothing from pipecat.utils.utils import exp_smoothing
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from loguru import logger
class AIService(FrameProcessor): class AIService(FrameProcessor):
@@ -230,12 +226,13 @@ class TTSService(AIService):
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
elif isinstance(frame, TTSSpeakFrame): elif isinstance(frame, TTSSpeakFrame):
await self._push_tts_frames(frame.text) await self._push_tts_frames(frame.text)
elif isinstance(frame, TTSModelUpdateFrame): elif isinstance(frame, TTSUpdateSettingsFrame):
await self.set_model(frame.model) if frame.model is not None:
elif isinstance(frame, TTSVoiceUpdateFrame): await self.set_model(frame.model)
await self.set_voice(frame.voice) if frame.voice is not None:
elif isinstance(frame, TTSLanguageUpdateFrame): await self.set_voice(frame.voice)
await self.set_language(frame.language) if frame.language is not None:
await self.set_language(frame.language)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
@@ -408,10 +405,11 @@ 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, STTModelUpdateFrame): elif isinstance(frame, STTUpdateSettingsFrame):
await self.set_model(frame.model) if frame.model is not None:
elif isinstance(frame, STTLanguageUpdateFrame): await self.set_model(frame.model)
await self.set_language(frame.language) if frame.language is not None:
await self.set_language(frame.language)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)

View File

@@ -5,47 +5,47 @@
# #
import base64 import base64
import json
import io
import copy import copy
from typing import Any, Dict, List, Optional import io
from dataclasses import dataclass import json
from PIL import Image
from asyncio import CancelledError
import re import re
from asyncio import CancelledError
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from loguru import logger
from PIL import Image
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pipecat.frames.frames import ( from pipecat.frames.frames import (
Frame, Frame,
LLMEnablePromptCachingFrame,
LLMModelUpdateFrame,
TextFrame,
VisionImageRawFrame,
UserImageRequestFrame,
UserImageRawFrame,
LLMMessagesFrame,
LLMFullResponseStartFrame,
LLMFullResponseEndFrame,
FunctionCallResultFrame,
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
FunctionCallResultFrame,
LLMEnablePromptCachingFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
LLMMessagesFrame,
LLMUpdateSettingsFrame,
StartInterruptionFrame, StartInterruptionFrame,
TextFrame,
UserImageRawFrame,
UserImageRequestFrame,
VisionImageRawFrame,
) )
from pipecat.metrics.metrics import LLMTokenUsage from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.aggregators.llm_response import (
from pipecat.services.ai_services import LLMService LLMAssistantContextAggregator,
LLMUserContextAggregator,
)
from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.aggregators.openai_llm_context import (
OpenAILLMContext, OpenAILLMContext,
OpenAILLMContextFrame, OpenAILLMContextFrame,
) )
from pipecat.processors.aggregators.llm_response import ( from pipecat.processors.frame_processor import FrameDirection
LLMUserContextAggregator, from pipecat.services.ai_services import LLMService
LLMAssistantContextAggregator,
)
from loguru import logger
try: try:
from anthropic import AsyncAnthropic, NOT_GIVEN, NotGiven from anthropic import NOT_GIVEN, AsyncAnthropic, NotGiven
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
logger.error(f"Exception: {e}") logger.error(f"Exception: {e}")
logger.error( logger.error(
@@ -293,9 +293,20 @@ 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, LLMModelUpdateFrame): elif isinstance(frame, LLMUpdateSettingsFrame):
logger.debug(f"Switching LLM model to: [{frame.model}]") if frame.model is not None:
self.set_model_name(frame.model) logger.debug(f"Switching LLM model to: [{frame.model}]")
self.set_model_name(frame.model)
if frame.max_tokens is not None:
await self.set_max_tokens(frame.max_tokens)
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)
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

@@ -17,7 +17,7 @@ from pipecat.frames.frames import (
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMModelUpdateFrame, LLMUpdateSettingsFrame,
TextFrame, TextFrame,
TTSAudioRawFrame, TTSAudioRawFrame,
TTSStartedFrame, TTSStartedFrame,
@@ -136,9 +136,10 @@ 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, LLMModelUpdateFrame): elif isinstance(frame, LLMUpdateSettingsFrame):
logger.debug(f"Switching LLM model to: [{frame.model}]") if frame.model is not None:
self._create_client(frame.model) 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

@@ -4,38 +4,39 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
import aiohttp
import base64 import base64
import io import io
import json import json
import httpx
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional from typing import Any, AsyncGenerator, Dict, List, Literal, Optional
import aiohttp
import httpx
from loguru import logger
from PIL import Image
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pipecat.frames.frames import ( from pipecat.frames.frames import (
ErrorFrame, ErrorFrame,
Frame, Frame,
FunctionCallInProgressFrame,
FunctionCallResultFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMModelUpdateFrame, LLMUpdateSettingsFrame,
StartInterruptionFrame,
TextFrame,
TTSAudioRawFrame, TTSAudioRawFrame,
TTSStartedFrame, TTSStartedFrame,
TTSStoppedFrame, TTSStoppedFrame,
TextFrame,
URLImageRawFrame, URLImageRawFrame,
VisionImageRawFrame, VisionImageRawFrame,
FunctionCallResultFrame,
FunctionCallInProgressFrame,
StartInterruptionFrame,
) )
from pipecat.metrics.metrics import LLMTokenUsage from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.llm_response import ( from pipecat.processors.aggregators.llm_response import (
LLMUserContextAggregator,
LLMAssistantContextAggregator, LLMAssistantContextAggregator,
LLMUserContextAggregator,
) )
from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.aggregators.openai_llm_context import (
OpenAILLMContext, OpenAILLMContext,
@@ -44,12 +45,14 @@ from pipecat.processors.aggregators.openai_llm_context import (
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.ai_services import ImageGenService, LLMService, TTSService from pipecat.services.ai_services import ImageGenService, LLMService, TTSService
from PIL import Image
from loguru import logger
try: try:
from openai import AsyncOpenAI, AsyncStream, DefaultAsyncHttpxClient, BadRequestError, NOT_GIVEN from openai import (
NOT_GIVEN,
AsyncOpenAI,
AsyncStream,
BadRequestError,
DefaultAsyncHttpxClient,
)
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
logger.error(f"Exception: {e}") logger.error(f"Exception: {e}")
@@ -280,9 +283,22 @@ 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, LLMModelUpdateFrame): elif isinstance(frame, LLMUpdateSettingsFrame):
logger.debug(f"Switching LLM model to: [{frame.model}]") if frame.model is not None:
self.set_model_name(frame.model) logger.debug(f"Switching LLM model to: [{frame.model}]")
self.set_model_name(frame.model)
if frame.frequency_penalty is not None:
await self.set_frequency_penalty(frame.frequency_penalty)
if frame.presence_penalty is not None:
await self.set_presence_penalty(frame.presence_penalty)
if frame.seed is not None:
await self.set_seed(frame.seed)
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)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
@@ -464,7 +480,7 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator):
await self._push_aggregation() await self._push_aggregation()
else: else:
logger.warning( logger.warning(
f"FunctionCallResultFrame tool_call_id does not match FunctionCallInProgressFrame tool_call_id" "FunctionCallResultFrame tool_call_id does not match FunctionCallInProgressFrame tool_call_id"
) )
self._function_call_in_progress = None self._function_call_in_progress = None
self._function_call_result = None self._function_call_result = None

View File

@@ -7,37 +7,36 @@
import json import json
import re import re
import uuid import uuid
from pydantic import BaseModel, Field
from typing import Any, Dict, List, Optional
from dataclasses import dataclass
from asyncio import CancelledError from asyncio import CancelledError
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from loguru import logger
from pydantic import BaseModel, Field
from pipecat.frames.frames import ( from pipecat.frames.frames import (
Frame, Frame,
LLMModelUpdateFrame, FunctionCallInProgressFrame,
FunctionCallResultFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
LLMMessagesFrame,
LLMUpdateSettingsFrame,
StartInterruptionFrame,
TextFrame, TextFrame,
UserImageRequestFrame, UserImageRequestFrame,
LLMMessagesFrame,
LLMFullResponseStartFrame,
LLMFullResponseEndFrame,
FunctionCallResultFrame,
FunctionCallInProgressFrame,
StartInterruptionFrame,
) )
from pipecat.metrics.metrics import LLMTokenUsage from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.aggregators.llm_response import (
from pipecat.services.ai_services import LLMService LLMAssistantContextAggregator,
LLMUserContextAggregator,
)
from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.aggregators.openai_llm_context import (
OpenAILLMContext, OpenAILLMContext,
OpenAILLMContextFrame, OpenAILLMContextFrame,
) )
from pipecat.processors.aggregators.llm_response import ( from pipecat.processors.frame_processor import FrameDirection
LLMUserContextAggregator, from pipecat.services.ai_services import LLMService
LLMAssistantContextAggregator,
)
from loguru import logger
try: try:
from together import AsyncTogether from together import AsyncTogether
@@ -188,7 +187,7 @@ class TogetherLLMService(LLMService):
if chunk.choices[0].finish_reason == "eos" and accumulating_function_call: if chunk.choices[0].finish_reason == "eos" and accumulating_function_call:
await self._extract_function_call(context, function_call_accumulator) await self._extract_function_call(context, function_call_accumulator)
except CancelledError as e: except CancelledError:
# todo: implement token counting estimates for use when the user interrupts a long generation # todo: implement token counting estimates for use when the user interrupts a long generation
# we do this in the anthropic.py service # we do this in the anthropic.py service
raise raise
@@ -206,9 +205,24 @@ class TogetherLLMService(LLMService):
context = frame.context context = frame.context
elif isinstance(frame, LLMMessagesFrame): elif isinstance(frame, LLMMessagesFrame):
context = TogetherLLMContext.from_messages(frame.messages) context = TogetherLLMContext.from_messages(frame.messages)
elif isinstance(frame, LLMModelUpdateFrame): elif isinstance(frame, LLMUpdateSettingsFrame):
logger.debug(f"Switching LLM model to: [{frame.model}]") if frame.model is not None:
self.set_model_name(frame.model) logger.debug(f"Switching LLM model to: [{frame.model}]")
self.set_model_name(frame.model)
if frame.frequency_penalty is not None:
await self.set_frequency_penalty(frame.frequency_penalty)
if frame.max_tokens is not None:
await self.set_max_tokens(frame.max_tokens)
if frame.presence_penalty is not None:
await self.set_presence_penalty(frame.presence_penalty)
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)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
@@ -338,7 +352,7 @@ class TogetherAssistantContextAggregator(LLMAssistantContextAggregator):
await self._push_aggregation() await self._push_aggregation()
else: else:
logger.warning( logger.warning(
f"FunctionCallResultFrame tool_call_id does not match FunctionCallInProgressFrame tool_call_id" "FunctionCallResultFrame tool_call_id does not match FunctionCallInProgressFrame tool_call_id"
) )
self._function_call_in_progress = None self._function_call_in_progress = None
self._function_call_result = None self._function_call_result = None