Add Anthropic docstrings
This commit is contained in:
@@ -4,6 +4,12 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
"""Anthropic AI service integration for Pipecat.
|
||||||
|
|
||||||
|
This module provides LLM services and context management for Anthropic's Claude models,
|
||||||
|
including support for function calling, vision, and prompt caching features.
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import copy
|
import copy
|
||||||
@@ -59,27 +65,66 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AnthropicContextAggregatorPair:
|
class AnthropicContextAggregatorPair:
|
||||||
|
"""Pair of context aggregators for Anthropic conversations.
|
||||||
|
|
||||||
|
Encapsulates both user and assistant context aggregators
|
||||||
|
to manage conversation flow and message formatting.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
_user: The user context aggregator.
|
||||||
|
_assistant: The assistant context aggregator.
|
||||||
|
"""
|
||||||
|
|
||||||
_user: "AnthropicUserContextAggregator"
|
_user: "AnthropicUserContextAggregator"
|
||||||
_assistant: "AnthropicAssistantContextAggregator"
|
_assistant: "AnthropicAssistantContextAggregator"
|
||||||
|
|
||||||
def user(self) -> "AnthropicUserContextAggregator":
|
def user(self) -> "AnthropicUserContextAggregator":
|
||||||
|
"""Get the user context aggregator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The user context aggregator instance.
|
||||||
|
"""
|
||||||
return self._user
|
return self._user
|
||||||
|
|
||||||
def assistant(self) -> "AnthropicAssistantContextAggregator":
|
def assistant(self) -> "AnthropicAssistantContextAggregator":
|
||||||
|
"""Get the assistant context aggregator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The assistant context aggregator instance.
|
||||||
|
"""
|
||||||
return self._assistant
|
return self._assistant
|
||||||
|
|
||||||
|
|
||||||
class AnthropicLLMService(LLMService):
|
class AnthropicLLMService(LLMService):
|
||||||
"""This class implements inference with Anthropic's AI models.
|
"""LLM service for Anthropic's Claude models.
|
||||||
|
|
||||||
Can provide a custom client via the `client` kwarg, allowing you to
|
Provides inference capabilities with Claude models including support for
|
||||||
use `AsyncAnthropicBedrock` and `AsyncAnthropicVertex` clients
|
function calling, vision processing, streaming responses, and prompt caching.
|
||||||
|
Can use custom clients like AsyncAnthropicBedrock and AsyncAnthropicVertex.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: Anthropic API key for authentication.
|
||||||
|
model: Model name to use. Defaults to "claude-sonnet-4-20250514".
|
||||||
|
params: Optional model parameters for inference.
|
||||||
|
client: Optional custom Anthropic client instance.
|
||||||
|
**kwargs: Additional arguments passed to parent LLMService.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Overriding the default adapter to use the Anthropic one.
|
# Overriding the default adapter to use the Anthropic one.
|
||||||
adapter_class = AnthropicLLMAdapter
|
adapter_class = AnthropicLLMAdapter
|
||||||
|
|
||||||
class InputParams(BaseModel):
|
class InputParams(BaseModel):
|
||||||
|
"""Input parameters for Anthropic model inference.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
enable_prompt_caching_beta: Whether to enable beta prompt caching feature.
|
||||||
|
max_tokens: Maximum tokens to generate. Must be at least 1.
|
||||||
|
temperature: Sampling temperature between 0.0 and 1.0.
|
||||||
|
top_k: Top-k sampling parameter.
|
||||||
|
top_p: Top-p sampling parameter between 0.0 and 1.0.
|
||||||
|
extra: Additional parameters to pass to the API.
|
||||||
|
"""
|
||||||
|
|
||||||
enable_prompt_caching_beta: Optional[bool] = False
|
enable_prompt_caching_beta: Optional[bool] = False
|
||||||
max_tokens: Optional[int] = Field(default_factory=lambda: 4096, ge=1)
|
max_tokens: Optional[int] = Field(default_factory=lambda: 4096, ge=1)
|
||||||
temperature: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0)
|
temperature: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0)
|
||||||
@@ -112,10 +157,20 @@ class AnthropicLLMService(LLMService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def can_generate_metrics(self) -> bool:
|
def can_generate_metrics(self) -> bool:
|
||||||
|
"""Check if this service can generate usage metrics.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True, as Anthropic provides detailed token usage metrics.
|
||||||
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enable_prompt_caching_beta(self) -> bool:
|
def enable_prompt_caching_beta(self) -> bool:
|
||||||
|
"""Check if prompt caching beta feature is enabled.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if prompt caching is enabled.
|
||||||
|
"""
|
||||||
return self._enable_prompt_caching_beta
|
return self._enable_prompt_caching_beta
|
||||||
|
|
||||||
def create_context_aggregator(
|
def create_context_aggregator(
|
||||||
@@ -125,22 +180,19 @@ class AnthropicLLMService(LLMService):
|
|||||||
user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(),
|
user_params: LLMUserAggregatorParams = LLMUserAggregatorParams(),
|
||||||
assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(),
|
assistant_params: LLMAssistantAggregatorParams = LLMAssistantAggregatorParams(),
|
||||||
) -> AnthropicContextAggregatorPair:
|
) -> AnthropicContextAggregatorPair:
|
||||||
"""Create an instance of AnthropicContextAggregatorPair from an
|
"""Create Anthropic-specific context aggregators.
|
||||||
OpenAILLMContext. Constructor keyword arguments for both the user and
|
|
||||||
assistant aggregators can be provided.
|
Creates a pair of context aggregators optimized for Anthropic's message format,
|
||||||
|
including support for function calls, tool usage, and image handling.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context (OpenAILLMContext): The LLM context.
|
context: The LLM context.
|
||||||
user_params (LLMUserAggregatorParams, optional): User aggregator
|
user_params: User aggregator parameters.
|
||||||
parameters.
|
assistant_params: Assistant aggregator parameters.
|
||||||
assistant_params (LLMAssistantAggregatorParams, optional): User
|
|
||||||
aggregator parameters.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
AnthropicContextAggregatorPair: A pair of context aggregators, one
|
A pair of context aggregators, one for the user and one for the assistant,
|
||||||
for the user and one for the assistant, encapsulated in an
|
encapsulated in an AnthropicContextAggregatorPair.
|
||||||
AnthropicContextAggregatorPair.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
context.set_llm_adapter(self.get_llm_adapter())
|
context.set_llm_adapter(self.get_llm_adapter())
|
||||||
|
|
||||||
@@ -310,6 +362,15 @@ class AnthropicLLMService(LLMService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
|
"""Process incoming frames and route them appropriately.
|
||||||
|
|
||||||
|
Handles various frame types including context frames, message frames,
|
||||||
|
vision frames, and settings updates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The frame to process.
|
||||||
|
direction: The direction of frame processing.
|
||||||
|
"""
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
context = None
|
context = None
|
||||||
@@ -361,6 +422,19 @@ class AnthropicLLMService(LLMService):
|
|||||||
|
|
||||||
|
|
||||||
class AnthropicLLMContext(OpenAILLMContext):
|
class AnthropicLLMContext(OpenAILLMContext):
|
||||||
|
"""LLM context specialized for Anthropic's message format and features.
|
||||||
|
|
||||||
|
Extends OpenAILLMContext to handle Anthropic-specific features like
|
||||||
|
system messages, prompt caching, and message format conversions.
|
||||||
|
Manages conversation state and message history formatting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: Initial list of conversation messages.
|
||||||
|
tools: Available function calling tools.
|
||||||
|
tool_choice: Tool selection preference.
|
||||||
|
system: System message content.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
messages: Optional[List[dict]] = None,
|
messages: Optional[List[dict]] = None,
|
||||||
@@ -381,6 +455,16 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def upgrade_to_anthropic(obj: OpenAILLMContext) -> "AnthropicLLMContext":
|
def upgrade_to_anthropic(obj: OpenAILLMContext) -> "AnthropicLLMContext":
|
||||||
|
"""Upgrade an OpenAI context to Anthropic format.
|
||||||
|
|
||||||
|
Converts message format and restructures content for Anthropic compatibility.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj: The OpenAI context to upgrade.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The upgraded Anthropic context.
|
||||||
|
"""
|
||||||
logger.debug(f"Upgrading to Anthropic: {obj}")
|
logger.debug(f"Upgrading to Anthropic: {obj}")
|
||||||
if isinstance(obj, OpenAILLMContext) and not isinstance(obj, AnthropicLLMContext):
|
if isinstance(obj, OpenAILLMContext) and not isinstance(obj, AnthropicLLMContext):
|
||||||
obj.__class__ = AnthropicLLMContext
|
obj.__class__ = AnthropicLLMContext
|
||||||
@@ -389,6 +473,14 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_openai_context(cls, openai_context: OpenAILLMContext):
|
def from_openai_context(cls, openai_context: OpenAILLMContext):
|
||||||
|
"""Create Anthropic context from OpenAI context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
openai_context: The OpenAI context to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
New Anthropic context with converted messages.
|
||||||
|
"""
|
||||||
self = cls(
|
self = cls(
|
||||||
messages=openai_context.messages,
|
messages=openai_context.messages,
|
||||||
tools=openai_context.tools,
|
tools=openai_context.tools,
|
||||||
@@ -400,12 +492,28 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_messages(cls, messages: List[dict]) -> "AnthropicLLMContext":
|
def from_messages(cls, messages: List[dict]) -> "AnthropicLLMContext":
|
||||||
|
"""Create context from a list of messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: List of conversation messages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
New Anthropic context with the provided messages.
|
||||||
|
"""
|
||||||
self = cls(messages=messages)
|
self = cls(messages=messages)
|
||||||
self._restructure_from_openai_messages()
|
self._restructure_from_openai_messages()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_image_frame(cls, frame: VisionImageRawFrame) -> "AnthropicLLMContext":
|
def from_image_frame(cls, frame: VisionImageRawFrame) -> "AnthropicLLMContext":
|
||||||
|
"""Create context from a vision image frame.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The vision image frame to process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
New Anthropic context with the image message.
|
||||||
|
"""
|
||||||
context = cls()
|
context = cls()
|
||||||
context.add_image_frame_message(
|
context.add_image_frame_message(
|
||||||
format=frame.format, size=frame.size, image=frame.image, text=frame.text
|
format=frame.format, size=frame.size, image=frame.image, text=frame.text
|
||||||
@@ -413,11 +521,15 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def set_messages(self, messages: List):
|
def set_messages(self, messages: List):
|
||||||
|
"""Set the messages list and reset cache tracking.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: New list of messages to set.
|
||||||
|
"""
|
||||||
self.turns_above_cache_threshold = 0
|
self.turns_above_cache_threshold = 0
|
||||||
self._messages[:] = messages
|
self._messages[:] = messages
|
||||||
self._restructure_from_openai_messages()
|
self._restructure_from_openai_messages()
|
||||||
|
|
||||||
# convert a message in Anthropic format into one or more messages in OpenAI format
|
|
||||||
def to_standard_messages(self, obj):
|
def to_standard_messages(self, obj):
|
||||||
"""Convert Anthropic message format to standard structured format.
|
"""Convert Anthropic message format to standard structured format.
|
||||||
|
|
||||||
@@ -558,6 +670,17 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
def add_image_frame_message(
|
def add_image_frame_message(
|
||||||
self, *, format: str, size: tuple[int, int], image: bytes, text: str = None
|
self, *, format: str, size: tuple[int, int], image: bytes, text: str = None
|
||||||
):
|
):
|
||||||
|
"""Add an image message to the context.
|
||||||
|
|
||||||
|
Converts the image to base64 JPEG format and adds it as a user message
|
||||||
|
with optional accompanying text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
format: The image format (e.g., 'RGB', 'RGBA').
|
||||||
|
size: Image dimensions as (width, height).
|
||||||
|
image: Raw image bytes.
|
||||||
|
text: Optional text to accompany the image.
|
||||||
|
"""
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
Image.frombytes(format, size, image).save(buffer, format="JPEG")
|
Image.frombytes(format, size, image).save(buffer, format="JPEG")
|
||||||
encoded_image = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
encoded_image = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||||
@@ -578,6 +701,14 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
self.add_message({"role": "user", "content": content})
|
self.add_message({"role": "user", "content": content})
|
||||||
|
|
||||||
def add_message(self, message):
|
def add_message(self, message):
|
||||||
|
"""Add a message to the context, merging with previous message if same role.
|
||||||
|
|
||||||
|
Anthropic requires alternating roles, so consecutive messages from the same
|
||||||
|
role are merged together.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message to add to the context.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if self.messages:
|
if self.messages:
|
||||||
# Anthropic requires that roles alternate. If this message's role is the same as the
|
# Anthropic requires that roles alternate. If this message's role is the same as the
|
||||||
@@ -603,6 +734,14 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
logger.error(f"Error adding message: {e}")
|
logger.error(f"Error adding message: {e}")
|
||||||
|
|
||||||
def get_messages_with_cache_control_markers(self) -> List[dict]:
|
def get_messages_with_cache_control_markers(self) -> List[dict]:
|
||||||
|
"""Get messages with prompt caching markers applied.
|
||||||
|
|
||||||
|
Adds cache control markers to appropriate messages based on the
|
||||||
|
number of turns above the cache threshold.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of messages with cache control markers added.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
messages = copy.deepcopy(self.messages)
|
messages = copy.deepcopy(self.messages)
|
||||||
if self.turns_above_cache_threshold >= 1 and messages[-1]["role"] == "user":
|
if self.turns_above_cache_threshold >= 1 and messages[-1]["role"] == "user":
|
||||||
@@ -670,12 +809,26 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
message["content"] = [{"type": "text", "text": "(empty)"}]
|
message["content"] = [{"type": "text", "text": "(empty)"}]
|
||||||
|
|
||||||
def get_messages_for_persistent_storage(self):
|
def get_messages_for_persistent_storage(self):
|
||||||
|
"""Get messages formatted for persistent storage.
|
||||||
|
|
||||||
|
Includes system message at the beginning if present.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of messages suitable for storage.
|
||||||
|
"""
|
||||||
messages = super().get_messages_for_persistent_storage()
|
messages = super().get_messages_for_persistent_storage()
|
||||||
if self.system:
|
if self.system:
|
||||||
messages.insert(0, {"role": "system", "content": self.system})
|
messages.insert(0, {"role": "system", "content": self.system})
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
def get_messages_for_logging(self) -> str:
|
def get_messages_for_logging(self) -> str:
|
||||||
|
"""Get messages formatted for logging with sensitive data redacted.
|
||||||
|
|
||||||
|
Replaces image data with placeholder text for cleaner logs.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON string representation of messages for logging.
|
||||||
|
"""
|
||||||
msgs = []
|
msgs = []
|
||||||
for message in self.messages:
|
for message in self.messages:
|
||||||
msg = copy.deepcopy(message)
|
msg = copy.deepcopy(message)
|
||||||
@@ -689,6 +842,12 @@ class AnthropicLLMContext(OpenAILLMContext):
|
|||||||
|
|
||||||
|
|
||||||
class AnthropicUserContextAggregator(LLMUserContextAggregator):
|
class AnthropicUserContextAggregator(LLMUserContextAggregator):
|
||||||
|
"""Anthropic-specific user context aggregator.
|
||||||
|
|
||||||
|
Handles aggregation of user messages for Anthropic LLM services.
|
||||||
|
Inherits all functionality from the base LLMUserContextAggregator.
|
||||||
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -703,7 +862,20 @@ class AnthropicUserContextAggregator(LLMUserContextAggregator):
|
|||||||
|
|
||||||
|
|
||||||
class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
|
class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
|
||||||
|
"""Context aggregator for assistant messages in Anthropic conversations.
|
||||||
|
|
||||||
|
Handles function call lifecycle management including in-progress tracking,
|
||||||
|
result handling, and cancellation for Anthropic's tool use format.
|
||||||
|
"""
|
||||||
|
|
||||||
async def handle_function_call_in_progress(self, frame: FunctionCallInProgressFrame):
|
async def handle_function_call_in_progress(self, frame: FunctionCallInProgressFrame):
|
||||||
|
"""Handle a function call that is starting.
|
||||||
|
|
||||||
|
Creates tool use message and placeholder tool result for tracking.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: Frame containing function call details.
|
||||||
|
"""
|
||||||
assistant_message = {"role": "assistant", "content": []}
|
assistant_message = {"role": "assistant", "content": []}
|
||||||
assistant_message["content"].append(
|
assistant_message["content"].append(
|
||||||
{
|
{
|
||||||
@@ -728,6 +900,13 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def handle_function_call_result(self, frame: FunctionCallResultFrame):
|
async def handle_function_call_result(self, frame: FunctionCallResultFrame):
|
||||||
|
"""Handle the result of a completed function call.
|
||||||
|
|
||||||
|
Updates the tool result with actual return value or completion status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: Frame containing function call result.
|
||||||
|
"""
|
||||||
if frame.result:
|
if frame.result:
|
||||||
result = json.dumps(frame.result)
|
result = json.dumps(frame.result)
|
||||||
await self._update_function_call_result(frame.function_name, frame.tool_call_id, result)
|
await self._update_function_call_result(frame.function_name, frame.tool_call_id, result)
|
||||||
@@ -737,6 +916,13 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def handle_function_call_cancel(self, frame: FunctionCallCancelFrame):
|
async def handle_function_call_cancel(self, frame: FunctionCallCancelFrame):
|
||||||
|
"""Handle cancellation of a function call.
|
||||||
|
|
||||||
|
Updates the tool result to indicate cancellation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: Frame containing function call cancellation details.
|
||||||
|
"""
|
||||||
await self._update_function_call_result(
|
await self._update_function_call_result(
|
||||||
frame.function_name, frame.tool_call_id, "CANCELLED"
|
frame.function_name, frame.tool_call_id, "CANCELLED"
|
||||||
)
|
)
|
||||||
@@ -755,6 +941,14 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
|
|||||||
content["content"] = result
|
content["content"] = result
|
||||||
|
|
||||||
async def handle_user_image_frame(self, frame: UserImageRawFrame):
|
async def handle_user_image_frame(self, frame: UserImageRawFrame):
|
||||||
|
"""Handle a user image frame with function call context.
|
||||||
|
|
||||||
|
Marks the associated function call as completed and adds the image
|
||||||
|
to the conversation context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: User image frame with request context.
|
||||||
|
"""
|
||||||
await self._update_function_call_result(
|
await self._update_function_call_result(
|
||||||
frame.request.function_name, frame.request.tool_call_id, "COMPLETED"
|
frame.request.function_name, frame.request.tool_call_id, "COMPLETED"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user