Merge pull request #2750 from pipecat-ai/pk/aws-nova-sonic-universal-llmcontext-1
Support new `LLMContext` pattern with `AWSNovaSonicLLMService`
This commit is contained in:
56
CHANGELOG.md
56
CHANGELOG.md
@@ -9,6 +9,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Expanded support for universal `LLMContext` to `AWSNovaSonicLLMService`.
|
||||||
|
As a reminder, the context-setup pattern when using `LLMContext` is:
|
||||||
|
|
||||||
|
```python
|
||||||
|
context = LLMContext(messages, tools)
|
||||||
|
context_aggregator = LLMContextAggregatorPair(context)
|
||||||
|
```
|
||||||
|
|
||||||
|
(Note that even though `AWSNovaSonicLLMService` now supports the universal
|
||||||
|
`LLMContext`, it is not meant to be swapped out for another LLM service at
|
||||||
|
runtime.)
|
||||||
|
|
||||||
|
Worth noting: whether or not you use the new context-setup pattern with
|
||||||
|
`AWSNovaSonicLLMService`, some types have changed under the hood:
|
||||||
|
|
||||||
|
```python
|
||||||
|
## BEFORE:
|
||||||
|
|
||||||
|
# Context aggregator type
|
||||||
|
context_aggregator: AWSNovaSonicContextAggregatorPair
|
||||||
|
|
||||||
|
# Context frame type
|
||||||
|
frame: OpenAILLMContextFrame
|
||||||
|
|
||||||
|
# Context type
|
||||||
|
context: AWSNovaSonicLLMContext
|
||||||
|
# or
|
||||||
|
context: OpenAILLMContext
|
||||||
|
|
||||||
|
# Reading messages from context
|
||||||
|
messages = context.messages
|
||||||
|
|
||||||
|
## AFTER:
|
||||||
|
|
||||||
|
# Context aggregator type
|
||||||
|
context_aggregator: LLMContextAggregatorPair
|
||||||
|
|
||||||
|
# Context frame type
|
||||||
|
frame: LLMContextFrame
|
||||||
|
|
||||||
|
# Context type
|
||||||
|
context: LLMContext
|
||||||
|
|
||||||
|
# Reading messages from context
|
||||||
|
messages = context.get_messages()
|
||||||
|
```
|
||||||
|
|
||||||
- Added support for `bulbul:v3` model in `SarvamTTSService` and `SarvamHttpTTSService`.
|
- Added support for `bulbul:v3` model in `SarvamTTSService` and `SarvamHttpTTSService`.
|
||||||
|
|
||||||
- Added `keyterms_prompt` parameter to `AssemblyAIConnectionParams`.
|
- Added `keyterms_prompt` parameter to `AssemblyAIConnectionParams`.
|
||||||
@@ -45,6 +92,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
- `SpeechmaticsSTTService` updated dependencies for `speechmatics-rt>=0.5.0`.
|
- `SpeechmaticsSTTService` updated dependencies for `speechmatics-rt>=0.5.0`.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- The `send_transcription_frames` argument to `AWSNovaSonicLLMService` is
|
||||||
|
deprecated. Transcription frames are now always sent. They go upstream, to be
|
||||||
|
handled by the user context aggregator. See "Changed" section for details.
|
||||||
|
|
||||||
|
- Types in `pipecat.services.aws.nova_sonic.context` have been deprecated due
|
||||||
|
to changes to support `LLMContext`. See "Changed" section for details.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed an issue in `RivaSegmentedSTTService` where a runtime error occurred due
|
- Fixed an issue in `RivaSegmentedSTTService` where a runtime error occurred due
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ async def save_conversation(params: FunctionCallParams):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with open(filename, "w") as file:
|
with open(filename, "w") as file:
|
||||||
# todo: extract 'system' into the first message in the list
|
|
||||||
messages = params.context.get_messages()
|
messages = params.context.get_messages()
|
||||||
# remove the last message, which is the instruction we just gave to save the conversation
|
# remove the last message, which is the instruction we just gave to save the conversation
|
||||||
messages.pop()
|
messages.pop()
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ async def save_conversation(params: FunctionCallParams):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with open(filename, "w") as file:
|
with open(filename, "w") as file:
|
||||||
# todo: extract 'system' into the first message in the list
|
|
||||||
messages = params.context.get_messages()
|
messages = params.context.get_messages()
|
||||||
# remove the last message (the instruction to save the context)
|
# remove the last message (the instruction to save the context)
|
||||||
messages.pop()
|
messages.pop()
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from pipecat.frames.frames import LLMRunFrame
|
|||||||
from pipecat.pipeline.pipeline import Pipeline
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||||
|
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
from pipecat.runner.types import RunnerArguments
|
from pipecat.runner.types import RunnerArguments
|
||||||
from pipecat.runner.utils import create_transport
|
from pipecat.runner.utils import create_transport
|
||||||
@@ -75,7 +77,7 @@ async def save_conversation(params: FunctionCallParams):
|
|||||||
filename = f"{BASE_FILENAME}{timestamp}.json"
|
filename = f"{BASE_FILENAME}{timestamp}.json"
|
||||||
try:
|
try:
|
||||||
with open(filename, "w") as file:
|
with open(filename, "w") as file:
|
||||||
messages = params.context.get_messages_for_persistent_storage()
|
messages = params.context.get_messages()
|
||||||
# remove the last few messages. in reverse order, they are:
|
# remove the last few messages. in reverse order, they are:
|
||||||
# - the in progress save tool call
|
# - the in progress save tool call
|
||||||
# - the invocation of the save tool call
|
# - the invocation of the save tool call
|
||||||
@@ -223,13 +225,13 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
llm.register_function("get_saved_conversation_filenames", get_saved_conversation_filenames)
|
llm.register_function("get_saved_conversation_filenames", get_saved_conversation_filenames)
|
||||||
llm.register_function("load_conversation", load_conversation)
|
llm.register_function("load_conversation", load_conversation)
|
||||||
|
|
||||||
context = OpenAILLMContext(
|
context = LLMContext(
|
||||||
messages=[
|
messages=[
|
||||||
{"role": "system", "content": f"{system_instruction}"},
|
{"role": "system", "content": f"{system_instruction}"},
|
||||||
],
|
],
|
||||||
tools=tools,
|
tools=tools,
|
||||||
)
|
)
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
context_aggregator = LLMContextAggregatorPair(context)
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from pipecat.frames.frames import LLMRunFrame
|
|||||||
from pipecat.pipeline.pipeline import Pipeline
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||||
|
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair
|
||||||
from pipecat.runner.types import RunnerArguments
|
from pipecat.runner.types import RunnerArguments
|
||||||
from pipecat.runner.utils import create_transport
|
from pipecat.runner.utils import create_transport
|
||||||
from pipecat.services.aws.nova_sonic.llm import AWSNovaSonicLLMService
|
from pipecat.services.aws.nova_sonic.llm import AWSNovaSonicLLMService
|
||||||
@@ -119,9 +120,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
llm.register_function("get_current_weather", fetch_weather_from_api)
|
llm.register_function("get_current_weather", fetch_weather_from_api)
|
||||||
|
|
||||||
# Set up context and context management.
|
# Set up context and context management.
|
||||||
# AWSNovaSonicService will adapt OpenAI LLM context objects with standard message format to
|
context = LLMContext(
|
||||||
# what's expected by Nova Sonic.
|
|
||||||
context = OpenAILLMContext(
|
|
||||||
messages=[
|
messages=[
|
||||||
{"role": "system", "content": f"{system_instruction}"},
|
{"role": "system", "content": f"{system_instruction}"},
|
||||||
{
|
{
|
||||||
@@ -131,7 +130,7 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
],
|
],
|
||||||
tools=tools,
|
tools=tools,
|
||||||
)
|
)
|
||||||
context_aggregator = llm.create_context_aggregator(context)
|
context_aggregator = LLMContextAggregatorPair(context)
|
||||||
|
|
||||||
# Build the pipeline
|
# Build the pipeline
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
|
|||||||
@@ -6,13 +6,47 @@
|
|||||||
|
|
||||||
"""AWS Nova Sonic LLM adapter for Pipecat."""
|
"""AWS Nova Sonic LLM adapter for Pipecat."""
|
||||||
|
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List, TypedDict
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, List, Optional, TypedDict
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
|
||||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
from pipecat.processors.aggregators.llm_context import LLMContext, LLMContextMessage
|
||||||
|
|
||||||
|
|
||||||
|
class Role(Enum):
|
||||||
|
"""Roles supported in AWS Nova Sonic conversations.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
SYSTEM: System-level messages (not used in conversation history).
|
||||||
|
USER: Messages sent by the user.
|
||||||
|
ASSISTANT: Messages sent by the assistant.
|
||||||
|
TOOL: Messages sent by tools (not used in conversation history).
|
||||||
|
"""
|
||||||
|
|
||||||
|
SYSTEM = "SYSTEM"
|
||||||
|
USER = "USER"
|
||||||
|
ASSISTANT = "ASSISTANT"
|
||||||
|
TOOL = "TOOL"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AWSNovaSonicConversationHistoryMessage:
|
||||||
|
"""A single message in AWS Nova Sonic conversation history.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
role: The role of the message sender (USER or ASSISTANT only).
|
||||||
|
text: The text content of the message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
role: Role # only USER and ASSISTANT
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
class AWSNovaSonicLLMInvocationParams(TypedDict):
|
class AWSNovaSonicLLMInvocationParams(TypedDict):
|
||||||
@@ -21,7 +55,9 @@ class AWSNovaSonicLLMInvocationParams(TypedDict):
|
|||||||
This is a placeholder until support for universal LLMContext machinery is added for AWS Nova Sonic.
|
This is a placeholder until support for universal LLMContext machinery is added for AWS Nova Sonic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
system_instruction: Optional[str]
|
||||||
|
messages: List[AWSNovaSonicConversationHistoryMessage]
|
||||||
|
tools: List[Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
||||||
@@ -34,7 +70,7 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
|||||||
@property
|
@property
|
||||||
def id_for_llm_specific_messages(self) -> str:
|
def id_for_llm_specific_messages(self) -> str:
|
||||||
"""Get the identifier used in LLMSpecificMessage instances for AWS Nova Sonic."""
|
"""Get the identifier used in LLMSpecificMessage instances for AWS Nova Sonic."""
|
||||||
raise NotImplementedError("Universal LLMContext is not yet supported for AWS Nova Sonic.")
|
return "aws-nova-sonic"
|
||||||
|
|
||||||
def get_llm_invocation_params(self, context: LLMContext) -> AWSNovaSonicLLMInvocationParams:
|
def get_llm_invocation_params(self, context: LLMContext) -> AWSNovaSonicLLMInvocationParams:
|
||||||
"""Get AWS Nova Sonic-specific LLM invocation parameters from a universal LLM context.
|
"""Get AWS Nova Sonic-specific LLM invocation parameters from a universal LLM context.
|
||||||
@@ -47,7 +83,13 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary of parameters for invoking AWS Nova Sonic's LLM API.
|
Dictionary of parameters for invoking AWS Nova Sonic's LLM API.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Universal LLMContext is not yet supported for AWS Nova Sonic.")
|
messages = self._from_universal_context_messages(self.get_messages(context))
|
||||||
|
return {
|
||||||
|
"system_instruction": messages.system_instruction,
|
||||||
|
"messages": messages.messages,
|
||||||
|
# NOTE: LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN)
|
||||||
|
"tools": self.from_standard_tools(context.tools) or [],
|
||||||
|
}
|
||||||
|
|
||||||
def get_messages_for_logging(self, context) -> List[Dict[str, Any]]:
|
def get_messages_for_logging(self, context) -> List[Dict[str, Any]]:
|
||||||
"""Get messages from a universal LLM context in a format ready for logging about AWS Nova Sonic.
|
"""Get messages from a universal LLM context in a format ready for logging about AWS Nova Sonic.
|
||||||
@@ -62,7 +104,75 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
|||||||
Returns:
|
Returns:
|
||||||
List of messages in a format ready for logging about AWS Nova Sonic.
|
List of messages in a format ready for logging about AWS Nova Sonic.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Universal LLMContext is not yet supported for AWS Nova Sonic.")
|
return self._from_universal_context_messages(self.get_messages(context)).messages
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ConvertedMessages:
|
||||||
|
"""Container for Google-formatted messages converted from universal context."""
|
||||||
|
|
||||||
|
messages: List[AWSNovaSonicConversationHistoryMessage]
|
||||||
|
system_instruction: Optional[str] = None
|
||||||
|
|
||||||
|
def _from_universal_context_messages(
|
||||||
|
self, universal_context_messages: List[LLMContextMessage]
|
||||||
|
) -> ConvertedMessages:
|
||||||
|
system_instruction = None
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
# Bail if there are no messages
|
||||||
|
if not universal_context_messages:
|
||||||
|
return self.ConvertedMessages()
|
||||||
|
|
||||||
|
universal_context_messages = copy.deepcopy(universal_context_messages)
|
||||||
|
|
||||||
|
# If we have a "system" message as our first message, let's pull that out into "instruction"
|
||||||
|
if universal_context_messages[0].get("role") == "system":
|
||||||
|
system = universal_context_messages.pop(0)
|
||||||
|
content = system.get("content")
|
||||||
|
if isinstance(content, str):
|
||||||
|
system_instruction = content
|
||||||
|
elif isinstance(content, list):
|
||||||
|
system_instruction = content[0].get("text")
|
||||||
|
if system_instruction:
|
||||||
|
self._system_instruction = system_instruction
|
||||||
|
|
||||||
|
# Process remaining messages to fill out conversation history.
|
||||||
|
# Nova Sonic supports "user" and "assistant" messages in history.
|
||||||
|
for universal_context_message in universal_context_messages:
|
||||||
|
message = self._from_universal_context_message(universal_context_message)
|
||||||
|
if message:
|
||||||
|
messages.append(message)
|
||||||
|
|
||||||
|
return self.ConvertedMessages(messages=messages, system_instruction=system_instruction)
|
||||||
|
|
||||||
|
def _from_universal_context_message(self, message) -> AWSNovaSonicConversationHistoryMessage:
|
||||||
|
"""Convert standard message format to Nova Sonic format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Standard message dictionary to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Nova Sonic conversation history message, or None if not convertible.
|
||||||
|
"""
|
||||||
|
role = message.get("role")
|
||||||
|
if message.get("role") == "user" or message.get("role") == "assistant":
|
||||||
|
content = message.get("content")
|
||||||
|
if isinstance(message.get("content"), list):
|
||||||
|
content = ""
|
||||||
|
for c in message.get("content"):
|
||||||
|
if c.get("type") == "text":
|
||||||
|
content += " " + c.get("text")
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Unhandled content type in context message: {c.get('type')} - {message}"
|
||||||
|
)
|
||||||
|
# There won't be content if this is an assistant tool call entry.
|
||||||
|
# We're ignoring those since they can't be loaded into AWS Nova Sonic conversation
|
||||||
|
# history
|
||||||
|
if content:
|
||||||
|
return AWSNovaSonicConversationHistoryMessage(role=Role[role.upper()], text=content)
|
||||||
|
# NOTE: we're ignoring messages with role "tool" since they can't be loaded into AWS Nova
|
||||||
|
# Sonic conversation history
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_aws_nova_sonic_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
def _to_aws_nova_sonic_function_format(function: FunctionSchema) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ service-specific adapter.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import copy
|
||||||
import io
|
import io
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, List, Optional, TypeAlias, Union
|
from typing import TYPE_CHECKING, Any, List, Optional, TypeAlias, Union
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from openai._types import NOT_GIVEN as OPEN_AI_NOT_GIVEN
|
from openai._types import NOT_GIVEN as OPEN_AI_NOT_GIVEN
|
||||||
@@ -31,6 +32,9 @@ from PIL import Image
|
|||||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||||
from pipecat.frames.frames import AudioRawFrame
|
from pipecat.frames.frames import AudioRawFrame
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
|
||||||
# "Re-export" types from OpenAI that we're using as universal context types.
|
# "Re-export" types from OpenAI that we're using as universal context types.
|
||||||
# NOTE: if universal message types need to someday diverge from OpenAI's, we
|
# NOTE: if universal message types need to someday diverge from OpenAI's, we
|
||||||
# should consider managing our own definitions. But we should do so carefully,
|
# should consider managing our own definitions. But we should do so carefully,
|
||||||
@@ -65,6 +69,26 @@ class LLMContext:
|
|||||||
and content formatting.
|
and content formatting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_openai_context(openai_context: "OpenAILLMContext") -> "LLMContext":
|
||||||
|
"""Create a universal LLM context from an OpenAI-specific context.
|
||||||
|
|
||||||
|
NOTE: this should only be used internally, for facilitating migration
|
||||||
|
from OpenAILLMContext to LLMContext. New user code should use
|
||||||
|
LLMContext directly.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
openai_context: The OpenAI LLM context to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
New LLMContext instance with converted messages and settings.
|
||||||
|
"""
|
||||||
|
return LLMContext(
|
||||||
|
messages=openai_context.get_messages(),
|
||||||
|
tools=openai_context.tools,
|
||||||
|
tool_choice=openai_context.tool_choice,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
messages: Optional[List[LLMContextMessage]] = None,
|
messages: Optional[List[LLMContextMessage]] = None,
|
||||||
|
|||||||
@@ -8,360 +8,80 @@
|
|||||||
|
|
||||||
This module provides specialized context aggregators and message handling for AWS Nova Sonic,
|
This module provides specialized context aggregators and message handling for AWS Nova Sonic,
|
||||||
including conversation history management and role-specific message processing.
|
including conversation history management and role-specific message processing.
|
||||||
|
|
||||||
|
.. deprecated:: 0.0.91
|
||||||
|
AWS Nova Sonic now supports `LLMContext` and `LLMContextAggregatorPair`.
|
||||||
|
Using the new patterns should allow you to not need types from this module.
|
||||||
|
|
||||||
|
BEFORE:
|
||||||
|
```
|
||||||
|
# Setup
|
||||||
|
context = OpenAILLMContext(messages, tools)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
# Context frame type
|
||||||
|
frame: OpenAILLMContextFrame
|
||||||
|
|
||||||
|
# Context type
|
||||||
|
context: AWSNovaSonicLLMContext
|
||||||
|
# or
|
||||||
|
context: OpenAILLMContext
|
||||||
|
|
||||||
|
# Reading messages from context
|
||||||
|
messages = context.messages
|
||||||
|
```
|
||||||
|
|
||||||
|
AFTER:
|
||||||
|
```
|
||||||
|
# Setup
|
||||||
|
context = LLMContext(messages, tools)
|
||||||
|
context_aggregator = LLMContextAggregatorPair(context)
|
||||||
|
|
||||||
|
# Context frame type
|
||||||
|
frame: LLMContextFrame
|
||||||
|
|
||||||
|
# Context type
|
||||||
|
context: LLMContext
|
||||||
|
|
||||||
|
# Reading messages from context
|
||||||
|
messages = context.get_messages()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import warnings
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from enum import Enum
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("always")
|
||||||
from loguru import logger
|
warnings.warn(
|
||||||
|
"Types in pipecat.services.aws.nova_sonic.context are deprecated. \n"
|
||||||
from pipecat.frames.frames import (
|
"AWS Nova Sonic now supports `LLMContext` and `LLMContextAggregatorPair`. \n"
|
||||||
BotStoppedSpeakingFrame,
|
"Using the new patterns should allow you to not need types from this module.\n\n"
|
||||||
DataFrame,
|
"BEFORE:\n"
|
||||||
Frame,
|
"```\n"
|
||||||
FunctionCallResultFrame,
|
"# Setup\n"
|
||||||
InterruptionFrame,
|
"context = OpenAILLMContext(messages, tools)\n"
|
||||||
LLMFullResponseEndFrame,
|
"context_aggregator = llm.create_context_aggregator(context)\n\n"
|
||||||
LLMFullResponseStartFrame,
|
"# Context frame type\n"
|
||||||
LLMMessagesAppendFrame,
|
"frame: OpenAILLMContextFrame\n\n"
|
||||||
LLMMessagesUpdateFrame,
|
"# Context type\n"
|
||||||
LLMSetToolChoiceFrame,
|
"context: AWSNovaSonicLLMContext\n"
|
||||||
LLMSetToolsFrame,
|
"# or\n"
|
||||||
TextFrame,
|
"context: OpenAILLMContext\n\n"
|
||||||
UserImageRawFrame,
|
"# Reading messages from context\n"
|
||||||
)
|
"messages = context.messages\n"
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
"```\n\n"
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
"AFTER:\n"
|
||||||
from pipecat.services.aws.nova_sonic.frames import AWSNovaSonicFunctionCallResultFrame
|
"```\n"
|
||||||
from pipecat.services.openai.llm import (
|
"# Setup\n"
|
||||||
OpenAIAssistantContextAggregator,
|
"context = LLMContext(messages, tools)\n"
|
||||||
OpenAIUserContextAggregator,
|
"context_aggregator = LLMContextAggregatorPair(context)\n\n"
|
||||||
)
|
"# Context frame type\n"
|
||||||
|
"frame: LLMContextFrame\n\n"
|
||||||
|
"# Context type\n"
|
||||||
class Role(Enum):
|
"context: LLMContext\n\n"
|
||||||
"""Roles supported in AWS Nova Sonic conversations.
|
"# Reading messages from context\n"
|
||||||
|
"messages = context.messages\n"
|
||||||
Parameters:
|
"```",
|
||||||
SYSTEM: System-level messages (not used in conversation history).
|
DeprecationWarning,
|
||||||
USER: Messages sent by the user.
|
stacklevel=2,
|
||||||
ASSISTANT: Messages sent by the assistant.
|
)
|
||||||
TOOL: Messages sent by tools (not used in conversation history).
|
|
||||||
"""
|
|
||||||
|
|
||||||
SYSTEM = "SYSTEM"
|
|
||||||
USER = "USER"
|
|
||||||
ASSISTANT = "ASSISTANT"
|
|
||||||
TOOL = "TOOL"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AWSNovaSonicConversationHistoryMessage:
|
|
||||||
"""A single message in AWS Nova Sonic conversation history.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
role: The role of the message sender (USER or ASSISTANT only).
|
|
||||||
text: The text content of the message.
|
|
||||||
"""
|
|
||||||
|
|
||||||
role: Role # only USER and ASSISTANT
|
|
||||||
text: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AWSNovaSonicConversationHistory:
|
|
||||||
"""Complete conversation history for AWS Nova Sonic initialization.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
system_instruction: System-level instruction for the conversation.
|
|
||||||
messages: List of conversation messages between user and assistant.
|
|
||||||
"""
|
|
||||||
|
|
||||||
system_instruction: str = None
|
|
||||||
messages: list[AWSNovaSonicConversationHistoryMessage] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
class AWSNovaSonicLLMContext(OpenAILLMContext):
|
|
||||||
"""Specialized LLM context for AWS Nova Sonic service.
|
|
||||||
|
|
||||||
Extends OpenAI context with Nova Sonic-specific message handling,
|
|
||||||
conversation history management, and text buffering capabilities.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, messages=None, tools=None, **kwargs):
|
|
||||||
"""Initialize AWS Nova Sonic LLM context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
messages: Initial messages for the context.
|
|
||||||
tools: Available tools for the context.
|
|
||||||
**kwargs: Additional arguments passed to parent class.
|
|
||||||
"""
|
|
||||||
super().__init__(messages=messages, tools=tools, **kwargs)
|
|
||||||
self.__setup_local()
|
|
||||||
|
|
||||||
def __setup_local(self, system_instruction: str = ""):
|
|
||||||
self._assistant_text = ""
|
|
||||||
self._user_text = ""
|
|
||||||
self._system_instruction = system_instruction
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def upgrade_to_nova_sonic(
|
|
||||||
obj: OpenAILLMContext, system_instruction: str
|
|
||||||
) -> "AWSNovaSonicLLMContext":
|
|
||||||
"""Upgrade an OpenAI context to AWS Nova Sonic context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj: The OpenAI context to upgrade.
|
|
||||||
system_instruction: System instruction for the context.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The upgraded AWS Nova Sonic context.
|
|
||||||
"""
|
|
||||||
if isinstance(obj, OpenAILLMContext) and not isinstance(obj, AWSNovaSonicLLMContext):
|
|
||||||
obj.__class__ = AWSNovaSonicLLMContext
|
|
||||||
obj.__setup_local(system_instruction)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# NOTE: this method has the side-effect of updating _system_instruction from messages
|
|
||||||
def get_messages_for_initializing_history(self) -> AWSNovaSonicConversationHistory:
|
|
||||||
"""Get conversation history for initializing AWS Nova Sonic session.
|
|
||||||
|
|
||||||
Processes stored messages and extracts system instruction and conversation
|
|
||||||
history in the format expected by AWS Nova Sonic.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Formatted conversation history with system instruction and messages.
|
|
||||||
"""
|
|
||||||
history = AWSNovaSonicConversationHistory(system_instruction=self._system_instruction)
|
|
||||||
|
|
||||||
# Bail if there are no messages
|
|
||||||
if not self.messages:
|
|
||||||
return history
|
|
||||||
|
|
||||||
messages = copy.deepcopy(self.messages)
|
|
||||||
|
|
||||||
# If we have a "system" message as our first message, let's pull that out into "instruction"
|
|
||||||
if messages[0].get("role") == "system":
|
|
||||||
system = messages.pop(0)
|
|
||||||
content = system.get("content")
|
|
||||||
if isinstance(content, str):
|
|
||||||
history.system_instruction = content
|
|
||||||
elif isinstance(content, list):
|
|
||||||
history.system_instruction = content[0].get("text")
|
|
||||||
if history.system_instruction:
|
|
||||||
self._system_instruction = history.system_instruction
|
|
||||||
|
|
||||||
# Process remaining messages to fill out conversation history.
|
|
||||||
# Nova Sonic supports "user" and "assistant" messages in history.
|
|
||||||
for message in messages:
|
|
||||||
history_message = self.from_standard_message(message)
|
|
||||||
if history_message:
|
|
||||||
history.messages.append(history_message)
|
|
||||||
|
|
||||||
return history
|
|
||||||
|
|
||||||
def get_messages_for_persistent_storage(self):
|
|
||||||
"""Get messages formatted for persistent storage.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of messages including system instruction if present.
|
|
||||||
"""
|
|
||||||
messages = super().get_messages_for_persistent_storage()
|
|
||||||
# If we have a system instruction and messages doesn't already contain it, add it
|
|
||||||
if self._system_instruction and not (messages and messages[0].get("role") == "system"):
|
|
||||||
messages.insert(0, {"role": "system", "content": self._system_instruction})
|
|
||||||
return messages
|
|
||||||
|
|
||||||
def from_standard_message(self, message) -> AWSNovaSonicConversationHistoryMessage:
|
|
||||||
"""Convert standard message format to Nova Sonic format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Standard message dictionary to convert.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Nova Sonic conversation history message, or None if not convertible.
|
|
||||||
"""
|
|
||||||
role = message.get("role")
|
|
||||||
if message.get("role") == "user" or message.get("role") == "assistant":
|
|
||||||
content = message.get("content")
|
|
||||||
if isinstance(message.get("content"), list):
|
|
||||||
content = ""
|
|
||||||
for c in message.get("content"):
|
|
||||||
if c.get("type") == "text":
|
|
||||||
content += " " + c.get("text")
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"Unhandled content type in context message: {c.get('type')} - {message}"
|
|
||||||
)
|
|
||||||
# There won't be content if this is an assistant tool call entry.
|
|
||||||
# We're ignoring those since they can't be loaded into AWS Nova Sonic conversation
|
|
||||||
# history
|
|
||||||
if content:
|
|
||||||
return AWSNovaSonicConversationHistoryMessage(role=Role[role.upper()], text=content)
|
|
||||||
# NOTE: we're ignoring messages with role "tool" since they can't be loaded into AWS Nova
|
|
||||||
# Sonic conversation history
|
|
||||||
|
|
||||||
def buffer_user_text(self, text):
|
|
||||||
"""Buffer user text for later flushing to context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: User text to buffer.
|
|
||||||
"""
|
|
||||||
self._user_text += f" {text}" if self._user_text else text
|
|
||||||
# logger.debug(f"User text buffered: {self._user_text}")
|
|
||||||
|
|
||||||
def flush_aggregated_user_text(self) -> str:
|
|
||||||
"""Flush buffered user text to context as a complete message.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The flushed user text, or empty string if no text was buffered.
|
|
||||||
"""
|
|
||||||
if not self._user_text:
|
|
||||||
return ""
|
|
||||||
user_text = self._user_text
|
|
||||||
message = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [{"type": "text", "text": user_text}],
|
|
||||||
}
|
|
||||||
self._user_text = ""
|
|
||||||
self.add_message(message)
|
|
||||||
# logger.debug(f"Context updated (user): {self.get_messages_for_logging()}")
|
|
||||||
return user_text
|
|
||||||
|
|
||||||
def buffer_assistant_text(self, text):
|
|
||||||
"""Buffer assistant text for later flushing to context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: Assistant text to buffer.
|
|
||||||
"""
|
|
||||||
self._assistant_text += text
|
|
||||||
# logger.debug(f"Assistant text buffered: {self._assistant_text}")
|
|
||||||
|
|
||||||
def flush_aggregated_assistant_text(self):
|
|
||||||
"""Flush buffered assistant text to context as a complete message."""
|
|
||||||
if not self._assistant_text:
|
|
||||||
return
|
|
||||||
message = {
|
|
||||||
"role": "assistant",
|
|
||||||
"content": [{"type": "text", "text": self._assistant_text}],
|
|
||||||
}
|
|
||||||
self._assistant_text = ""
|
|
||||||
self.add_message(message)
|
|
||||||
# logger.debug(f"Context updated (assistant): {self.get_messages_for_logging()}")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AWSNovaSonicMessagesUpdateFrame(DataFrame):
|
|
||||||
"""Frame containing updated AWS Nova Sonic context.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
context: The updated AWS Nova Sonic LLM context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
context: AWSNovaSonicLLMContext
|
|
||||||
|
|
||||||
|
|
||||||
class AWSNovaSonicUserContextAggregator(OpenAIUserContextAggregator):
|
|
||||||
"""Context aggregator for user messages in AWS Nova Sonic conversations.
|
|
||||||
|
|
||||||
Extends the OpenAI user context aggregator to emit Nova Sonic-specific
|
|
||||||
context update frames.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def process_frame(
|
|
||||||
self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM
|
|
||||||
):
|
|
||||||
"""Process frames and emit Nova Sonic-specific context updates.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The frame to process.
|
|
||||||
direction: The direction the frame is traveling.
|
|
||||||
"""
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
# Parent does not push LLMMessagesUpdateFrame
|
|
||||||
if isinstance(frame, LLMMessagesUpdateFrame):
|
|
||||||
await self.push_frame(AWSNovaSonicMessagesUpdateFrame(context=self._context))
|
|
||||||
|
|
||||||
|
|
||||||
class AWSNovaSonicAssistantContextAggregator(OpenAIAssistantContextAggregator):
|
|
||||||
"""Context aggregator for assistant messages in AWS Nova Sonic conversations.
|
|
||||||
|
|
||||||
Provides specialized handling for assistant responses and function calls
|
|
||||||
in AWS Nova Sonic context, with custom frame processing logic.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
|
||||||
"""Process frames with Nova Sonic-specific logic.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The frame to process.
|
|
||||||
direction: The direction the frame is traveling.
|
|
||||||
"""
|
|
||||||
# HACK: For now, disable the context aggregator by making it just pass through all frames
|
|
||||||
# that the parent handles (except the function call stuff, which we still need).
|
|
||||||
# For an explanation of this hack, see
|
|
||||||
# AWSNovaSonicLLMService._report_assistant_response_text_added.
|
|
||||||
if isinstance(
|
|
||||||
frame,
|
|
||||||
(
|
|
||||||
InterruptionFrame,
|
|
||||||
LLMFullResponseStartFrame,
|
|
||||||
LLMFullResponseEndFrame,
|
|
||||||
TextFrame,
|
|
||||||
LLMMessagesAppendFrame,
|
|
||||||
LLMMessagesUpdateFrame,
|
|
||||||
LLMSetToolsFrame,
|
|
||||||
LLMSetToolChoiceFrame,
|
|
||||||
UserImageRawFrame,
|
|
||||||
BotStoppedSpeakingFrame,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
await self.push_frame(frame, direction)
|
|
||||||
else:
|
|
||||||
await super().process_frame(frame, direction)
|
|
||||||
|
|
||||||
async def handle_function_call_result(self, frame: FunctionCallResultFrame):
|
|
||||||
"""Handle function call results for AWS Nova Sonic.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame: The function call result frame to handle.
|
|
||||||
"""
|
|
||||||
await super().handle_function_call_result(frame)
|
|
||||||
|
|
||||||
# The standard function callback code path pushes the FunctionCallResultFrame from the LLM
|
|
||||||
# itself, so we didn't have a chance to add the result to the AWS Nova Sonic server-side
|
|
||||||
# context. Let's push a special frame to do that.
|
|
||||||
await self.push_frame(
|
|
||||||
AWSNovaSonicFunctionCallResultFrame(result_frame=frame), FrameDirection.UPSTREAM
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AWSNovaSonicContextAggregatorPair:
|
|
||||||
"""Pair of user and assistant context aggregators for AWS Nova Sonic.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
_user: The user context aggregator.
|
|
||||||
_assistant: The assistant context aggregator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_user: AWSNovaSonicUserContextAggregator
|
|
||||||
_assistant: AWSNovaSonicAssistantContextAggregator
|
|
||||||
|
|
||||||
def user(self) -> AWSNovaSonicUserContextAggregator:
|
|
||||||
"""Get the user context aggregator.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The user context aggregator instance.
|
|
||||||
"""
|
|
||||||
return self._user
|
|
||||||
|
|
||||||
def assistant(self) -> AWSNovaSonicAssistantContextAggregator:
|
|
||||||
"""Get the assistant context aggregator.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The assistant context aggregator instance.
|
|
||||||
"""
|
|
||||||
return self._assistant
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from loguru import logger
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||||
from pipecat.adapters.services.aws_nova_sonic_adapter import AWSNovaSonicLLMAdapter
|
from pipecat.adapters.services.aws_nova_sonic_adapter import AWSNovaSonicLLMAdapter, Role
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
BotStoppedSpeakingFrame,
|
BotStoppedSpeakingFrame,
|
||||||
CancelFrame,
|
CancelFrame,
|
||||||
@@ -33,35 +33,30 @@ from pipecat.frames.frames import (
|
|||||||
Frame,
|
Frame,
|
||||||
FunctionCallFromLLM,
|
FunctionCallFromLLM,
|
||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
InterimTranscriptionFrame,
|
InterruptionFrame,
|
||||||
LLMContextFrame,
|
LLMContextFrame,
|
||||||
LLMFullResponseEndFrame,
|
LLMFullResponseEndFrame,
|
||||||
LLMFullResponseStartFrame,
|
LLMFullResponseStartFrame,
|
||||||
LLMTextFrame,
|
|
||||||
StartFrame,
|
StartFrame,
|
||||||
TranscriptionFrame,
|
TranscriptionFrame,
|
||||||
TTSAudioRawFrame,
|
TTSAudioRawFrame,
|
||||||
TTSStartedFrame,
|
TTSStartedFrame,
|
||||||
TTSStoppedFrame,
|
TTSStoppedFrame,
|
||||||
TTSTextFrame,
|
TTSTextFrame,
|
||||||
|
UserStartedSpeakingFrame,
|
||||||
|
UserStoppedSpeakingFrame,
|
||||||
)
|
)
|
||||||
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||||
from pipecat.processors.aggregators.llm_response import (
|
from pipecat.processors.aggregators.llm_response import (
|
||||||
LLMAssistantAggregatorParams,
|
LLMAssistantAggregatorParams,
|
||||||
LLMUserAggregatorParams,
|
LLMUserAggregatorParams,
|
||||||
)
|
)
|
||||||
|
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair
|
||||||
from pipecat.processors.aggregators.openai_llm_context import (
|
from pipecat.processors.aggregators.openai_llm_context import (
|
||||||
OpenAILLMContext,
|
OpenAILLMContext,
|
||||||
OpenAILLMContextFrame,
|
OpenAILLMContextFrame,
|
||||||
)
|
)
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.processors.frame_processor import FrameDirection
|
||||||
from pipecat.services.aws.nova_sonic.context import (
|
|
||||||
AWSNovaSonicAssistantContextAggregator,
|
|
||||||
AWSNovaSonicContextAggregatorPair,
|
|
||||||
AWSNovaSonicLLMContext,
|
|
||||||
AWSNovaSonicUserContextAggregator,
|
|
||||||
Role,
|
|
||||||
)
|
|
||||||
from pipecat.services.aws.nova_sonic.frames import AWSNovaSonicFunctionCallResultFrame
|
|
||||||
from pipecat.services.llm_service import LLMService
|
from pipecat.services.llm_service import LLMService
|
||||||
from pipecat.utils.time import time_now_iso8601
|
from pipecat.utils.time import time_now_iso8601
|
||||||
|
|
||||||
@@ -217,6 +212,11 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
system_instruction: System-level instruction for the model.
|
system_instruction: System-level instruction for the model.
|
||||||
tools: Available tools/functions for the model to use.
|
tools: Available tools/functions for the model to use.
|
||||||
send_transcription_frames: Whether to emit transcription frames.
|
send_transcription_frames: Whether to emit transcription frames.
|
||||||
|
|
||||||
|
.. deprecated:: 0.0.91
|
||||||
|
This parameter is deprecated and will be removed in a future version.
|
||||||
|
Transcription frames are always sent.
|
||||||
|
|
||||||
**kwargs: Additional arguments passed to the parent LLMService.
|
**kwargs: Additional arguments passed to the parent LLMService.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@@ -230,8 +230,20 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
self._params = params or Params()
|
self._params = params or Params()
|
||||||
self._system_instruction = system_instruction
|
self._system_instruction = system_instruction
|
||||||
self._tools = tools
|
self._tools = tools
|
||||||
self._send_transcription_frames = send_transcription_frames
|
|
||||||
self._context: Optional[AWSNovaSonicLLMContext] = None
|
if not send_transcription_frames:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
warnings.warn(
|
||||||
|
"`send_transcription_frames` is deprecated and will be removed in a future version. "
|
||||||
|
"Transcription frames are always sent.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._context: Optional[LLMContext] = None
|
||||||
self._stream: Optional[
|
self._stream: Optional[
|
||||||
DuplexEventStream[
|
DuplexEventStream[
|
||||||
InvokeModelWithBidirectionalStreamInput,
|
InvokeModelWithBidirectionalStreamInput,
|
||||||
@@ -244,12 +256,17 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
self._input_audio_content_name: Optional[str] = None
|
self._input_audio_content_name: Optional[str] = None
|
||||||
self._content_being_received: Optional[CurrentContent] = None
|
self._content_being_received: Optional[CurrentContent] = None
|
||||||
self._assistant_is_responding = False
|
self._assistant_is_responding = False
|
||||||
|
self._may_need_repush_assistant_text = False
|
||||||
self._ready_to_send_context = False
|
self._ready_to_send_context = False
|
||||||
self._handling_bot_stopped_speaking = False
|
self._handling_bot_stopped_speaking = False
|
||||||
self._triggering_assistant_response = False
|
self._triggering_assistant_response = False
|
||||||
|
self._waiting_for_trigger_transcription = False
|
||||||
self._disconnecting = False
|
self._disconnecting = False
|
||||||
self._connected_time: Optional[float] = None
|
self._connected_time: Optional[float] = None
|
||||||
self._wants_connection = False
|
self._wants_connection = False
|
||||||
|
self._user_text_buffer = ""
|
||||||
|
self._assistant_text_buffer = ""
|
||||||
|
self._completed_tool_calls = set()
|
||||||
|
|
||||||
file_path = files("pipecat.services.aws.nova_sonic").joinpath("ready.wav")
|
file_path = files("pipecat.services.aws.nova_sonic").joinpath("ready.wav")
|
||||||
with wave.open(file_path.open("rb"), "rb") as wav_file:
|
with wave.open(file_path.open("rb"), "rb") as wav_file:
|
||||||
@@ -302,12 +319,12 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
logger.debug("Resetting conversation")
|
logger.debug("Resetting conversation")
|
||||||
await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=False)
|
await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=False)
|
||||||
|
|
||||||
# Carry over previous context through disconnect
|
# Grab context to carry through disconnect/reconnect
|
||||||
context = self._context
|
context = self._context
|
||||||
await self._disconnect()
|
|
||||||
self._context = context
|
|
||||||
|
|
||||||
|
await self._disconnect()
|
||||||
await self._start_connecting()
|
await self._start_connecting()
|
||||||
|
await self._handle_context(context)
|
||||||
|
|
||||||
#
|
#
|
||||||
# frame processing
|
# frame processing
|
||||||
@@ -322,28 +339,35 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
"""
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
if isinstance(frame, OpenAILLMContextFrame):
|
if isinstance(frame, (LLMContextFrame, OpenAILLMContextFrame)):
|
||||||
await self._handle_context(frame.context)
|
context = (
|
||||||
elif isinstance(frame, LLMContextFrame):
|
frame.context
|
||||||
raise NotImplementedError(
|
if isinstance(frame, LLMContextFrame)
|
||||||
"Universal LLMContext is not yet supported for AWS Nova Sonic."
|
else LLMContext.from_openai_context(frame.context)
|
||||||
)
|
)
|
||||||
|
await self._handle_context(context)
|
||||||
elif isinstance(frame, InputAudioRawFrame):
|
elif isinstance(frame, InputAudioRawFrame):
|
||||||
await self._handle_input_audio_frame(frame)
|
await self._handle_input_audio_frame(frame)
|
||||||
elif isinstance(frame, BotStoppedSpeakingFrame):
|
elif isinstance(frame, BotStoppedSpeakingFrame):
|
||||||
await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=True)
|
await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=True)
|
||||||
elif isinstance(frame, AWSNovaSonicFunctionCallResultFrame):
|
elif isinstance(frame, InterruptionFrame):
|
||||||
await self._handle_function_call_result(frame)
|
await self._handle_interruption_frame()
|
||||||
|
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
async def _handle_context(self, context: OpenAILLMContext):
|
async def _handle_context(self, context: LLMContext):
|
||||||
|
if self._disconnecting:
|
||||||
|
return
|
||||||
|
|
||||||
if not self._context:
|
if not self._context:
|
||||||
# We got our initial context - try to finish connecting
|
# We got our initial context
|
||||||
self._context = AWSNovaSonicLLMContext.upgrade_to_nova_sonic(
|
# Try to finish connecting
|
||||||
context, self._system_instruction
|
self._context = context
|
||||||
)
|
|
||||||
await self._finish_connecting_if_context_available()
|
await self._finish_connecting_if_context_available()
|
||||||
|
else:
|
||||||
|
# We got an updated context
|
||||||
|
# Send results for any newly-completed function calls
|
||||||
|
await self._process_completed_function_calls(send_new_results=True)
|
||||||
|
|
||||||
async def _handle_input_audio_frame(self, frame: InputAudioRawFrame):
|
async def _handle_input_audio_frame(self, frame: InputAudioRawFrame):
|
||||||
# Wait until we're done sending the assistant response trigger audio before sending audio
|
# Wait until we're done sending the assistant response trigger audio before sending audio
|
||||||
@@ -393,9 +417,9 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
else:
|
else:
|
||||||
await finalize_assistant_response()
|
await finalize_assistant_response()
|
||||||
|
|
||||||
async def _handle_function_call_result(self, frame: AWSNovaSonicFunctionCallResultFrame):
|
async def _handle_interruption_frame(self):
|
||||||
result = frame.result_frame
|
if self._assistant_is_responding:
|
||||||
await self._send_tool_result(tool_call_id=result.tool_call_id, result=result.result)
|
self._may_need_repush_assistant_text = True
|
||||||
|
|
||||||
#
|
#
|
||||||
# LLM communication: lifecycle
|
# LLM communication: lifecycle
|
||||||
@@ -431,6 +455,17 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
logger.error(f"{self} initialization error: {e}")
|
logger.error(f"{self} initialization error: {e}")
|
||||||
await self._disconnect()
|
await self._disconnect()
|
||||||
|
|
||||||
|
async def _process_completed_function_calls(self, send_new_results: bool):
|
||||||
|
# Check for set of completed function calls in the context
|
||||||
|
for message in self._context.get_messages():
|
||||||
|
if message.get("role") and message.get("content") != "IN_PROGRESS":
|
||||||
|
tool_call_id = message.get("tool_call_id")
|
||||||
|
if tool_call_id and tool_call_id not in self._completed_tool_calls:
|
||||||
|
# Found a newly-completed function call - send the result to the service
|
||||||
|
if send_new_results:
|
||||||
|
await self._send_tool_result(tool_call_id, message.get("content"))
|
||||||
|
self._completed_tool_calls.add(tool_call_id)
|
||||||
|
|
||||||
async def _finish_connecting_if_context_available(self):
|
async def _finish_connecting_if_context_available(self):
|
||||||
# We can only finish connecting once we've gotten our initial context and we're ready to
|
# We can only finish connecting once we've gotten our initial context and we're ready to
|
||||||
# send it
|
# send it
|
||||||
@@ -439,30 +474,38 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
|
|
||||||
logger.info("Finishing connecting (setting up session)...")
|
logger.info("Finishing connecting (setting up session)...")
|
||||||
|
|
||||||
|
# Initialize our bookkeeping of already-completed tool calls in the
|
||||||
|
# context
|
||||||
|
await self._process_completed_function_calls(send_new_results=False)
|
||||||
|
|
||||||
# Read context
|
# Read context
|
||||||
history = self._context.get_messages_for_initializing_history()
|
adapter: AWSNovaSonicLLMAdapter = self.get_llm_adapter()
|
||||||
|
llm_connection_params = adapter.get_llm_invocation_params(self._context)
|
||||||
|
|
||||||
# Send prompt start event, specifying tools.
|
# Send prompt start event, specifying tools.
|
||||||
# Tools from context take priority over self._tools.
|
# Tools from context take priority over self._tools.
|
||||||
tools = (
|
tools = (
|
||||||
self._context.tools
|
llm_connection_params["tools"]
|
||||||
if self._context.tools
|
if llm_connection_params["tools"]
|
||||||
else self.get_llm_adapter().from_standard_tools(self._tools)
|
else adapter.from_standard_tools(self._tools)
|
||||||
)
|
)
|
||||||
logger.debug(f"Using tools: {tools}")
|
logger.debug(f"Using tools: {tools}")
|
||||||
await self._send_prompt_start_event(tools)
|
await self._send_prompt_start_event(tools)
|
||||||
|
|
||||||
# Send system instruction.
|
# Send system instruction.
|
||||||
# Instruction from context takes priority over self._system_instruction.
|
# Instruction from context takes priority over self._system_instruction.
|
||||||
# (NOTE: this prioritizing occurred automatically behind the scenes: the context was
|
system_instruction = (
|
||||||
# initialized with self._system_instruction and then updated itself from its messages when
|
llm_connection_params["system_instruction"]
|
||||||
# get_messages_for_initializing_history() was called).
|
if llm_connection_params["system_instruction"]
|
||||||
logger.debug(f"Using system instruction: {history.system_instruction}")
|
else self._system_instruction
|
||||||
if history.system_instruction:
|
)
|
||||||
await self._send_text_event(text=history.system_instruction, role=Role.SYSTEM)
|
logger.debug(f"Using system instruction: {system_instruction}")
|
||||||
|
if system_instruction:
|
||||||
|
await self._send_text_event(text=system_instruction, role=Role.SYSTEM)
|
||||||
|
|
||||||
# Send conversation history
|
# Send conversation history
|
||||||
for message in history.messages:
|
for message in llm_connection_params["messages"]:
|
||||||
|
# logger.debug(f"Seeding conversation history with message: {message}")
|
||||||
await self._send_text_event(text=message.text, role=message.role)
|
await self._send_text_event(text=message.text, role=message.role)
|
||||||
|
|
||||||
# Start audio input
|
# Start audio input
|
||||||
@@ -492,9 +535,12 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
await self._send_session_end_events()
|
await self._send_session_end_events()
|
||||||
self._client = None
|
self._client = None
|
||||||
|
|
||||||
|
# Clean up context
|
||||||
|
self._context = None
|
||||||
|
|
||||||
# Clean up stream
|
# Clean up stream
|
||||||
if self._stream:
|
if self._stream:
|
||||||
await self._stream.input_stream.close()
|
await self._stream.close()
|
||||||
self._stream = None
|
self._stream = None
|
||||||
|
|
||||||
# NOTE: see explanation of HACK, below
|
# NOTE: see explanation of HACK, below
|
||||||
@@ -510,15 +556,23 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
self._receive_task = None
|
self._receive_task = None
|
||||||
|
|
||||||
# Reset remaining connection-specific state
|
# Reset remaining connection-specific state
|
||||||
|
# Should be all private state except:
|
||||||
|
# - _wants_connection
|
||||||
|
# - _assistant_response_trigger_audio
|
||||||
self._prompt_name = None
|
self._prompt_name = None
|
||||||
self._input_audio_content_name = None
|
self._input_audio_content_name = None
|
||||||
self._content_being_received = None
|
self._content_being_received = None
|
||||||
self._assistant_is_responding = False
|
self._assistant_is_responding = False
|
||||||
|
self._may_need_repush_assistant_text = False
|
||||||
self._ready_to_send_context = False
|
self._ready_to_send_context = False
|
||||||
self._handling_bot_stopped_speaking = False
|
self._handling_bot_stopped_speaking = False
|
||||||
self._triggering_assistant_response = False
|
self._triggering_assistant_response = False
|
||||||
|
self._waiting_for_trigger_transcription = False
|
||||||
self._disconnecting = False
|
self._disconnecting = False
|
||||||
self._connected_time = None
|
self._connected_time = None
|
||||||
|
self._user_text_buffer = ""
|
||||||
|
self._assistant_text_buffer = ""
|
||||||
|
self._completed_tool_calls = set()
|
||||||
|
|
||||||
logger.info("Finished disconnecting")
|
logger.info("Finished disconnecting")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -826,6 +880,10 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
# Handle the LLM completion ending
|
# Handle the LLM completion ending
|
||||||
await self._handle_completion_end_event(event_json)
|
await self._handle_completion_end_event(event_json)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if self._disconnecting:
|
||||||
|
# Errors are kind of expected while disconnecting, so just
|
||||||
|
# ignore them and do nothing
|
||||||
|
return
|
||||||
logger.error(f"{self} error processing responses: {e}")
|
logger.error(f"{self} error processing responses: {e}")
|
||||||
if self._wants_connection:
|
if self._wants_connection:
|
||||||
await self.reset_conversation()
|
await self.reset_conversation()
|
||||||
@@ -956,7 +1014,7 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
async def _report_assistant_response_started(self):
|
async def _report_assistant_response_started(self):
|
||||||
logger.debug("Assistant response started")
|
logger.debug("Assistant response started")
|
||||||
|
|
||||||
# Report that the assistant has started their response.
|
# Report the start of the assistant response.
|
||||||
await self.push_frame(LLMFullResponseStartFrame())
|
await self.push_frame(LLMFullResponseStartFrame())
|
||||||
|
|
||||||
# Report that equivalent of TTS (this is a speech-to-speech model) started
|
# Report that equivalent of TTS (this is a speech-to-speech model) started
|
||||||
@@ -968,23 +1026,16 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
|
|
||||||
logger.debug(f"Assistant response text added: {text}")
|
logger.debug(f"Assistant response text added: {text}")
|
||||||
|
|
||||||
# Report some text added to the ongoing assistant response
|
# Report the text of the assistant response.
|
||||||
await self.push_frame(LLMTextFrame(text))
|
|
||||||
|
|
||||||
# Report some text added to the *equivalent* of TTS (this is a speech-to-speech model)
|
|
||||||
await self.push_frame(TTSTextFrame(text))
|
await self.push_frame(TTSTextFrame(text))
|
||||||
|
|
||||||
# TODO: this is a (hopefully temporary) HACK. Here we directly manipulate the context rather
|
# HACK: here we're also buffering the assistant text ourselves as a
|
||||||
# than relying on the frames pushed to the assistant context aggregator. The pattern of
|
# backup rather than relying solely on the assistant context aggregator
|
||||||
# receiving full-sentence text after the assistant has spoken does not easily fit with the
|
# to do it, because the text arrives from Nova Sonic only after all the
|
||||||
# Pipecat expectation of chunks of text streaming in while the assistant is speaking.
|
# assistant audio frames have been pushed, meaning that if an
|
||||||
# Interruption handling was especially challenging. Rather than spend days trying to fit a
|
# interruption frame were to arrive we would lose all of it (the text
|
||||||
# square peg in a round hole, I decided on this hack for the time being. We can most cleanly
|
# frames sitting in the queue would be wiped).
|
||||||
# abandon this hack if/when AWS Nova Sonic implements streaming smaller text chunks
|
self._assistant_text_buffer += text
|
||||||
# interspersed with audio. Note that when we move away from this hack, we need to make sure
|
|
||||||
# that on an interruption we avoid sending LLMFullResponseEndFrame, which gets the
|
|
||||||
# LLMAssistantContextAggregator into a bad state.
|
|
||||||
self._context.buffer_assistant_text(text)
|
|
||||||
|
|
||||||
async def _report_assistant_response_ended(self):
|
async def _report_assistant_response_ended(self):
|
||||||
if not self._context: # should never happen
|
if not self._context: # should never happen
|
||||||
@@ -992,14 +1043,34 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
|
|
||||||
logger.debug("Assistant response ended")
|
logger.debug("Assistant response ended")
|
||||||
|
|
||||||
# Report that the assistant has finished their response.
|
# If an interruption frame arrived while the assistant was responding
|
||||||
|
# we may have lost all of the assistant text (see HACK, above), so
|
||||||
|
# re-push it downstream to the aggregator now.
|
||||||
|
if self._may_need_repush_assistant_text:
|
||||||
|
# Just in case, check that assistant text hasn't already made it
|
||||||
|
# into the context (sometimes it does, despite the interruption).
|
||||||
|
messages = self._context.get_messages()
|
||||||
|
last_message = messages[-1] if messages else None
|
||||||
|
if (
|
||||||
|
not last_message
|
||||||
|
or last_message.get("role") != "assistant"
|
||||||
|
or last_message.get("content") != self._assistant_text_buffer
|
||||||
|
):
|
||||||
|
# We also need to re-push the LLMFullResponseStartFrame since the
|
||||||
|
# TTSTextFrame would be ignored otherwise (the interruption frame
|
||||||
|
# would have cleared the assistant aggregator state).
|
||||||
|
await self.push_frame(LLMFullResponseStartFrame())
|
||||||
|
await self.push_frame(TTSTextFrame(self._assistant_text_buffer))
|
||||||
|
self._may_need_repush_assistant_text = False
|
||||||
|
|
||||||
|
# Report the end of the assistant response.
|
||||||
await self.push_frame(LLMFullResponseEndFrame())
|
await self.push_frame(LLMFullResponseEndFrame())
|
||||||
|
|
||||||
# Report that equivalent of TTS (this is a speech-to-speech model) stopped.
|
# Report that equivalent of TTS (this is a speech-to-speech model) stopped.
|
||||||
await self.push_frame(TTSStoppedFrame())
|
await self.push_frame(TTSStoppedFrame())
|
||||||
|
|
||||||
# For an explanation of this hack, see _report_assistant_response_text_added.
|
# Clear out the buffered assistant text
|
||||||
self._context.flush_aggregated_assistant_text()
|
self._assistant_text_buffer = ""
|
||||||
|
|
||||||
#
|
#
|
||||||
# user transcription reporting
|
# user transcription reporting
|
||||||
@@ -1016,33 +1087,67 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
|
|
||||||
logger.debug(f"User transcription text added: {text}")
|
logger.debug(f"User transcription text added: {text}")
|
||||||
|
|
||||||
# Manually add new user transcription text to context.
|
# HACK: here we're buffering the user text ourselves rather than
|
||||||
# We can't rely on the user context aggregator to do this since it's upstream from the LLM.
|
# relying on the upstream user context aggregator to do it, because the
|
||||||
self._context.buffer_user_text(text)
|
# text arrives in fairly large chunks spaced fairly far apart in time.
|
||||||
|
# That means the user text would be split between different messages in
|
||||||
# Report that some new user transcription text is available.
|
# context. Even if we sent placeholder InterimTranscriptionFrames in
|
||||||
if self._send_transcription_frames:
|
# between each TranscriptionFrame to tell the aggregator to hold off on
|
||||||
await self.push_frame(
|
# finalizing the user message, the aggregator would likely get the last
|
||||||
InterimTranscriptionFrame(text=text, user_id="", timestamp=time_now_iso8601())
|
# chunk too late.
|
||||||
)
|
self._user_text_buffer += f" {text}" if self._user_text_buffer else text
|
||||||
|
|
||||||
async def _report_user_transcription_ended(self):
|
async def _report_user_transcription_ended(self):
|
||||||
if not self._context: # should never happen
|
if not self._context: # should never happen
|
||||||
return
|
return
|
||||||
|
|
||||||
# Manually add user transcription to context (if any has been buffered).
|
|
||||||
# We can't rely on the user context aggregator to do this since it's upstream from the LLM.
|
|
||||||
transcription = self._context.flush_aggregated_user_text()
|
|
||||||
|
|
||||||
if not transcription:
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.debug(f"User transcription ended")
|
logger.debug(f"User transcription ended")
|
||||||
|
|
||||||
if self._send_transcription_frames:
|
# Report to the upstream user context aggregator that some new user
|
||||||
await self.push_frame(
|
# transcription text is available.
|
||||||
TranscriptionFrame(text=transcription, user_id="", timestamp=time_now_iso8601())
|
|
||||||
|
# HACK: Check if this transcription was triggered by our own
|
||||||
|
# assistant response trigger. If so, we need to wrap it with
|
||||||
|
# UserStarted/StoppedSpeakingFrames; otherwise the user aggregator
|
||||||
|
# would fire an EmulatedUserStartedSpeakingFrame, which would
|
||||||
|
# trigger an interruption, which would prevent us from writing the
|
||||||
|
# assistant response to context.
|
||||||
|
#
|
||||||
|
# Sending an EmulateUserStartedSpeakingFrame ourselves doesn't
|
||||||
|
# work: it just causes the interruption we're trying to avoid.
|
||||||
|
#
|
||||||
|
# Setting enable_emulated_vad_interruptions also doesn't work: at
|
||||||
|
# the time the user aggregator receives the TranscriptionFrame, it
|
||||||
|
# doesn't yet know the assistant has started responding, so it
|
||||||
|
# doesn't know that emulating the user starting to speak would
|
||||||
|
# cause an interruption.
|
||||||
|
should_wrap_in_user_started_stopped_speaking_frames = (
|
||||||
|
self._waiting_for_trigger_transcription
|
||||||
|
and self._user_text_buffer.strip().lower() == "ready"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start wrapping the upstream transcription in UserStarted/StoppedSpeakingFrames if needed
|
||||||
|
if should_wrap_in_user_started_stopped_speaking_frames:
|
||||||
|
logger.debug(
|
||||||
|
"Wrapping assistant response trigger transcription with upstream UserStarted/StoppedSpeakingFrames"
|
||||||
)
|
)
|
||||||
|
await self.push_frame(UserStartedSpeakingFrame(), direction=FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
|
# Send the transcription upstream for the user context aggregator
|
||||||
|
frame = TranscriptionFrame(
|
||||||
|
text=self._user_text_buffer, user_id="", timestamp=time_now_iso8601()
|
||||||
|
)
|
||||||
|
await self.push_frame(frame, direction=FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
|
# Finish wrapping the upstream transcription in UserStarted/StoppedSpeakingFrames if needed
|
||||||
|
if should_wrap_in_user_started_stopped_speaking_frames:
|
||||||
|
await self.push_frame(UserStoppedSpeakingFrame(), direction=FrameDirection.UPSTREAM)
|
||||||
|
|
||||||
|
# Clear out the buffered user text
|
||||||
|
self._user_text_buffer = ""
|
||||||
|
|
||||||
|
# We're no longer waiting for a trigger transcription
|
||||||
|
self._waiting_for_trigger_transcription = False
|
||||||
|
|
||||||
#
|
#
|
||||||
# context
|
# context
|
||||||
@@ -1054,23 +1159,26 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
*,
|
*,
|
||||||
user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(),
|
user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(),
|
||||||
assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(),
|
assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(),
|
||||||
) -> AWSNovaSonicContextAggregatorPair:
|
) -> LLMContextAggregatorPair:
|
||||||
"""Create context aggregator pair for managing conversation context.
|
"""Create context aggregator pair for managing conversation context.
|
||||||
|
|
||||||
|
NOTE: this method exists only for backward compatibility. New code
|
||||||
|
should instead do:
|
||||||
|
context = LLMContext(...)
|
||||||
|
context_aggregator = LLMContextAggregatorPair(context)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context: The OpenAI LLM context to upgrade.
|
context: The OpenAI LLM context.
|
||||||
user_params: Parameters for the user context aggregator.
|
user_params: Parameters for the user context aggregator.
|
||||||
assistant_params: Parameters for the assistant context aggregator.
|
assistant_params: Parameters for the assistant context aggregator.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A pair of user and assistant context aggregators.
|
A pair of user and assistant context aggregators.
|
||||||
"""
|
"""
|
||||||
context.set_llm_adapter(self.get_llm_adapter())
|
context = LLMContext.from_openai_context(context)
|
||||||
|
return LLMContextAggregatorPair(
|
||||||
user = AWSNovaSonicUserContextAggregator(context=context, params=user_params)
|
context, user_params=user_params, assistant_params=assistant_params
|
||||||
assistant = AWSNovaSonicAssistantContextAggregator(context=context, params=assistant_params)
|
)
|
||||||
|
|
||||||
return AWSNovaSonicContextAggregatorPair(user, assistant)
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# assistant response trigger (HACK)
|
# assistant response trigger (HACK)
|
||||||
@@ -1108,6 +1216,8 @@ class AWSNovaSonicLLMService(LLMService):
|
|||||||
try:
|
try:
|
||||||
logger.debug("Sending assistant response trigger...")
|
logger.debug("Sending assistant response trigger...")
|
||||||
|
|
||||||
|
self._waiting_for_trigger_transcription = True
|
||||||
|
|
||||||
chunk_duration = 0.02 # what we might get from InputAudioRawFrame
|
chunk_duration = 0.02 # what we might get from InputAudioRawFrame
|
||||||
chunk_size = int(
|
chunk_size = int(
|
||||||
chunk_duration
|
chunk_duration
|
||||||
|
|||||||
@@ -8,18 +8,80 @@
|
|||||||
|
|
||||||
This module provides specialized context aggregators and message handling for AWS Nova Sonic,
|
This module provides specialized context aggregators and message handling for AWS Nova Sonic,
|
||||||
including conversation history management and role-specific message processing.
|
including conversation history management and role-specific message processing.
|
||||||
|
|
||||||
|
.. deprecated:: 0.0.91
|
||||||
|
AWS Nova Sonic now supports `LLMContext` and `LLMContextAggregatorPair`.
|
||||||
|
Using the new patterns should allow you to not need types from this module.
|
||||||
|
|
||||||
|
BEFORE:
|
||||||
|
```
|
||||||
|
# Setup
|
||||||
|
context = OpenAILLMContext(messages, tools)
|
||||||
|
context_aggregator = llm.create_context_aggregator(context)
|
||||||
|
|
||||||
|
# Context frame type
|
||||||
|
frame: OpenAILLMContextFrame
|
||||||
|
|
||||||
|
# Context type
|
||||||
|
context: AWSNovaSonicLLMContext
|
||||||
|
# or
|
||||||
|
context: OpenAILLMContext
|
||||||
|
|
||||||
|
# Reading messages from context
|
||||||
|
messages = context.messages
|
||||||
|
```
|
||||||
|
|
||||||
|
AFTER:
|
||||||
|
```
|
||||||
|
# Setup
|
||||||
|
context = LLMContext(messages, tools)
|
||||||
|
context_aggregator = LLMContextAggregatorPair(context)
|
||||||
|
|
||||||
|
# Context frame type
|
||||||
|
frame: LLMContextFrame
|
||||||
|
|
||||||
|
# Context type
|
||||||
|
context: LLMContext
|
||||||
|
|
||||||
|
# Reading messages from context
|
||||||
|
messages = context.get_messages()
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from pipecat.services.aws.nova_sonic.context import *
|
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("always")
|
warnings.simplefilter("always")
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Types in pipecat.services.aws_nova_sonic.context are deprecated. "
|
"Types in pipecat.services.aws_nova_sonic.context are deprecated. \n"
|
||||||
"Please use the equivalent types from "
|
"AWS Nova Sonic now supports `LLMContext` and `LLMContextAggregatorPair`. \n"
|
||||||
"pipecat.services.aws.nova_sonic.context instead.",
|
"Using the new patterns should allow you to not need types from this module.\n\n"
|
||||||
|
"BEFORE:\n"
|
||||||
|
"```\n"
|
||||||
|
"# Setup\n"
|
||||||
|
"context = OpenAILLMContext(messages, tools)\n"
|
||||||
|
"context_aggregator = llm.create_context_aggregator(context)\n\n"
|
||||||
|
"# Context frame type\n"
|
||||||
|
"frame: OpenAILLMContextFrame\n\n"
|
||||||
|
"# Context type\n"
|
||||||
|
"context: AWSNovaSonicLLMContext\n"
|
||||||
|
"# or\n"
|
||||||
|
"context: OpenAILLMContext\n\n"
|
||||||
|
"# Reading messages from context\n"
|
||||||
|
"messages = context.messages\n"
|
||||||
|
"```\n\n"
|
||||||
|
"AFTER:\n"
|
||||||
|
"```\n"
|
||||||
|
"# Setup\n"
|
||||||
|
"context = LLMContext(messages, tools)\n"
|
||||||
|
"context_aggregator = LLMContextAggregatorPair(context)\n\n"
|
||||||
|
"# Context frame type\n"
|
||||||
|
"frame: LLMContextFrame\n\n"
|
||||||
|
"# Context type\n"
|
||||||
|
"context: LLMContext\n\n"
|
||||||
|
"# Reading messages from context\n"
|
||||||
|
"messages = context.messages\n"
|
||||||
|
"```",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user