From 03a067d3e62ec72f17cd5aa5505d9dedf57f9ee3 Mon Sep 17 00:00:00 2001 From: jhpiedrahitao Date: Wed, 18 Jun 2025 10:50:42 -0500 Subject: [PATCH 1/6] add sambanova llm and stt --- README.md | 4 +- docs/api/requirements.txt | 1 + dot-env.template | 5 +- pyproject.toml | 1 + src/pipecat/services/sambanova/__init__.py | 14 ++ src/pipecat/services/sambanova/llm.py | 168 +++++++++++++++++++++ src/pipecat/services/sambanova/stt.py | 65 ++++++++ 7 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 src/pipecat/services/sambanova/__init__.py create mode 100644 src/pipecat/services/sambanova/llm.py create mode 100644 src/pipecat/services/sambanova/stt.py diff --git a/README.md b/README.md index 7ec4c6000..2d14f37c9 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ You can connect to Pipecat from any platform using our official SDKs: | Category | Services | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [Parakeet (NVIDIA)](https://docs.pipecat.ai/server/services/stt/parakeet), [Ultravox](https://docs.pipecat.ai/server/services/stt/ultravox), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) | -| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [Together AI](https://docs.pipecat.ai/server/services/llm/together) | +| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [Parakeet (NVIDIA)](https://docs.pipecat.ai/server/services/stt/parakeet), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova) [Ultravox](https://docs.pipecat.ai/server/services/stt/ultravox), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) | +| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova) [Together AI](https://docs.pipecat.ai/server/services/llm/together) | | Text-to-Speech | [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [FastPitch (NVIDIA)](https://docs.pipecat.ai/server/services/tts/fastpitch), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) | | Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai) | | Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local | diff --git a/docs/api/requirements.txt b/docs/api/requirements.txt index a77ff1084..d783b33e8 100644 --- a/docs/api/requirements.txt +++ b/docs/api/requirements.txt @@ -42,6 +42,7 @@ pipecat-ai[openai] pipecat-ai[qwen] pipecat-ai[remote-smart-turn] # pipecat-ai[riva] # Mocked +pipecat-ai[sambanova] pipecat-ai[silero] pipecat-ai[simli] pipecat-ai[soundfile] diff --git a/dot-env.template b/dot-env.template index 20d73b3ad..210654f1f 100644 --- a/dot-env.template +++ b/dot-env.template @@ -107,4 +107,7 @@ MINIMAX_API_KEY=... MINIMAX_GROUP_ID=... # Sarvam AI -SARVAM_API_KEY=... \ No newline at end of file +SARVAM_API_KEY=... + +# SambaNova +SAMBANOVA_API_KEY=... \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4652b684a..cafb4fd2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ playht = [ "pyht~=0.1.12", "websockets~=13.1" ] qwen = [] rime = [ "websockets~=13.1" ] riva = [ "nvidia-riva-client~=2.19.1" ] +sambanova = [] sentry = [ "sentry-sdk~=2.23.1" ] local-smart-turn = [ "coremltools>=8.0", "transformers", "torch==2.5.0", "torchaudio==2.5.0" ] remote-smart-turn = [] diff --git a/src/pipecat/services/sambanova/__init__.py b/src/pipecat/services/sambanova/__init__.py new file mode 100644 index 000000000..8dbcb522a --- /dev/null +++ b/src/pipecat/services/sambanova/__init__.py @@ -0,0 +1,14 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import sys + +from pipecat.services import DeprecatedModuleProxy + +from .llm import * +from .stt import * + +sys.modules[__name__] = DeprecatedModuleProxy(globals(), "sambanova", "sambanova.[llm,stt,tts]") diff --git a/src/pipecat/services/sambanova/llm.py b/src/pipecat/services/sambanova/llm.py new file mode 100644 index 000000000..3f96e2653 --- /dev/null +++ b/src/pipecat/services/sambanova/llm.py @@ -0,0 +1,168 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import json +from typing import Any, Dict, List, Optional + +from loguru import logger +from openai import AsyncStream +from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam +from pipecat.frames.frames import ( + LLMTextFrame, +) +from pipecat.metrics.metrics import LLMTokenUsage +from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.llm_service import FunctionCallFromLLM +from pipecat.services.openai.llm import OpenAILLMService +from pipecat.utils.tracing.service_decorators import traced_llm + + +class SambaNovaLLMService(OpenAILLMService): # type: ignore + """A service for interacting with SambaNova using the OpenAI-compatible interface. + This service extends OpenAILLMService to connect to SambaNova's API endpoint while + maintaining full compatibility with OpenAI's interface and functionality. + Args: + api_key (str): The API key for accessing SambaNova API. + model (str, optional): The model identifier to use. Defaults to "Meta-Llama-3.3-70B-Instruct". + base_url (str, optional): The base URL for SambaNova API. Defaults to "https://api.sambanova.ai/v1". + **kwargs: Additional keyword arguments passed to OpenAILLMService. + """ + + def __init__( + self, + *, + api_key: str, + model: str = 'Llama-4-Maverick-17B-128E-Instruct', + base_url: str = 'https://api.sambanova.ai/v1', + **kwargs: Dict[Any, Any], + ) -> None: + super().__init__(api_key=api_key, base_url=base_url, model=model, **kwargs) + + def create_client( + self, api_key: Optional[str] = None, base_url: Optional[str] = None, **kwargs: Dict[Any, Any] + ) -> Any: + """Create OpenAI-compatible client for SambaNova API endpoint.""" + + logger.debug(f'Creating SambaNova client with API {base_url}') + return super().create_client(api_key, base_url, **kwargs) + + async def get_chat_completions(self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]) -> Any: + """Get chat completions from SambaNova API endpoint.""" + + params = { + 'model': self.model_name, + 'stream': True, + 'messages': messages, + 'tools': context.tools, + 'tool_choice': context.tool_choice, + 'stream_options': {'include_usage': True}, + 'temperature': self._settings['temperature'], + 'top_p': self._settings['top_p'], + 'max_tokens': self._settings['max_tokens'], + 'max_completion_tokens': self._settings['max_completion_tokens'], + } + + params.update(self._settings['extra']) + + chunks = await self._client.chat.completions.create(**params) + return chunks + + @traced_llm # type: ignore + async def _process_context(self, context: OpenAILLMContext) -> AsyncStream[ChatCompletionChunk]: + """Redefine this method until SambaNova API introduces indexing in tool calls.""" + + functions_list = [] + arguments_list = [] + tool_id_list = [] + func_idx = 0 + function_name = '' + arguments = '' + tool_call_id = '' + + await self.start_ttfb_metrics() + + chunk_stream: AsyncStream[ChatCompletionChunk] = await self._stream_chat_completions(context) + + async for chunk in chunk_stream: + if chunk.usage: + tokens = LLMTokenUsage( + prompt_tokens=chunk.usage.prompt_tokens, + completion_tokens=chunk.usage.completion_tokens, + total_tokens=chunk.usage.total_tokens, + ) + await self.start_llm_usage_metrics(tokens) + + if chunk.choices is None or len(chunk.choices) == 0: + continue + + await self.stop_ttfb_metrics() + + if not chunk.choices[0].delta: + continue + + if chunk.choices[0].delta.tool_calls: + # We're streaming the LLM response to enable the fastest response times. + # For text, we just yield each chunk as we receive it and count on consumers + # to do whatever coalescing they need (eg. to pass full sentences to TTS) + # + # If the LLM is a function call, we'll do some coalescing here. + # If the response contains a function name, we'll yield a frame to tell consumers + # that they can start preparing to call the function with that name. + # We accumulate all the arguments for the rest of the streamed response, then when + # the response is done, we package up all the arguments and the function name and + # yield a frame containing the function name and the arguments. + + tool_call = chunk.choices[0].delta.tool_calls[0] + if tool_call.index != func_idx: + functions_list.append(function_name) + arguments_list.append(arguments) + tool_id_list.append(tool_call_id) + function_name = '' + arguments = '' + tool_call_id = '' + func_idx += 1 + if tool_call.function and tool_call.function.name: + function_name += tool_call.function.name + tool_call_id = tool_call.id # type: ignore + if tool_call.function and tool_call.function.arguments: + # Keep iterating through the response to collect all the argument fragments + arguments += tool_call.function.arguments + elif chunk.choices[0].delta.content: + await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content)) + + # When gpt-4o-audio / gpt-4o-mini-audio is used for llm or stt+llm + # we need to get LLMTextFrame for the transcript + elif hasattr(chunk.choices[0].delta, 'audio') and chunk.choices[0].delta.audio.get('transcript'): + await self.push_frame(LLMTextFrame(chunk.choices[0].delta.audio['transcript'])) + + # if we got a function name and arguments, check to see if it's a function with + # a registered handler. If so, run the registered callback, save the result to + # the context, and re-prompt to get a chat answer. If we don't have a registered + # handler, raise an exception. + if function_name and arguments: + # added to the list as last function name and arguments not added to the list + functions_list.append(function_name) + arguments_list.append(arguments) + tool_id_list.append(tool_call_id) + + function_calls = [] + + for function_name, arguments, tool_id in zip(functions_list, arguments_list, tool_id_list): + # This allows compatibility until SambaNova API introduces indexing in tool calls. + if len(arguments) < 1: + continue + + arguments = json.loads(arguments) + function_calls.append( + FunctionCallFromLLM( + context=context, + tool_call_id=tool_id, + function_name=function_name, + arguments=arguments, + ) + ) + + await self.run_function_calls(function_calls) \ No newline at end of file diff --git a/src/pipecat/services/sambanova/stt.py b/src/pipecat/services/sambanova/stt.py new file mode 100644 index 000000000..63520410e --- /dev/null +++ b/src/pipecat/services/sambanova/stt.py @@ -0,0 +1,65 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +from typing import Any, Optional + +from pipecat.services.whisper.base_stt import BaseWhisperSTTService, Transcription +from pipecat.transcriptions.language import Language + + +class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore + """SambaNova Whisper speech-to-text service. + Uses SambaNova's Whisper API to convert audio to text. + Requires a SambaNova API key set via the api_key parameter or SAMBANOVA_API_KEY environment variable. + Args: + model: Whisper model to use. Defaults to "Whisper-Large-v3". + api_key: SambaNova API key. Defaults to None. + base_url: API base URL. Defaults to "https://api.sambanova.ai/v1". + language: Language of the audio input. Defaults to English. + prompt: Optional text to guide the model's style or continue a previous segment. + temperature: Optional sampling temperature between 0 and 1. Defaults to 0.0. + **kwargs: Additional arguments passed to `pipecat.services.whisper.base_stt.BaseWhisperSTTService`. + """ + + def __init__( + self, + *, + model: str = 'Whisper-Large-v3', + api_key: Optional[str] = None, + base_url: str = 'https://api.sambanova.ai/v1', + language: Optional[Language] = Language.EN, + prompt: Optional[str] = None, + temperature: Optional[float] = None, + **kwargs: Any, + ) -> None: + super().__init__( + model=model, + api_key=api_key, + base_url=base_url, + language=language, + prompt=prompt, + temperature=temperature, + **kwargs, + ) + + async def _transcribe(self, audio: bytes) -> Transcription: + assert self._language is not None # Assigned in the BaseWhisperSTTService class + + # Build kwargs dict with only set parameters + kwargs = { + 'file': ('audio.wav', audio, 'audio/wav'), + 'model': self.model_name, + 'response_format': 'json', + 'language': self._language, + } + + if self._prompt is not None: + kwargs['prompt'] = self._prompt + + if self._temperature is not None: + kwargs['temperature'] = self._temperature + + return await self._client.audio.transcriptions.create(**kwargs) \ No newline at end of file From fae2d272d561352f721a5dda49573a9d70a9e189 Mon Sep 17 00:00:00 2001 From: jhpiedrahitao Date: Wed, 18 Jun 2025 10:53:06 -0500 Subject: [PATCH 2/6] fmt --- src/pipecat/services/sambanova/llm.py | 66 ++++++++++++++++----------- src/pipecat/services/sambanova/stt.py | 18 ++++---- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/pipecat/services/sambanova/llm.py b/src/pipecat/services/sambanova/llm.py index 3f96e2653..01a8d294c 100644 --- a/src/pipecat/services/sambanova/llm.py +++ b/src/pipecat/services/sambanova/llm.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional from loguru import logger from openai import AsyncStream from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam + from pipecat.frames.frames import ( LLMTextFrame, ) @@ -35,37 +36,42 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore self, *, api_key: str, - model: str = 'Llama-4-Maverick-17B-128E-Instruct', - base_url: str = 'https://api.sambanova.ai/v1', + model: str = "Llama-4-Maverick-17B-128E-Instruct", + base_url: str = "https://api.sambanova.ai/v1", **kwargs: Dict[Any, Any], ) -> None: super().__init__(api_key=api_key, base_url=base_url, model=model, **kwargs) def create_client( - self, api_key: Optional[str] = None, base_url: Optional[str] = None, **kwargs: Dict[Any, Any] + self, + api_key: Optional[str] = None, + base_url: Optional[str] = None, + **kwargs: Dict[Any, Any], ) -> Any: """Create OpenAI-compatible client for SambaNova API endpoint.""" - logger.debug(f'Creating SambaNova client with API {base_url}') + logger.debug(f"Creating SambaNova client with API {base_url}") return super().create_client(api_key, base_url, **kwargs) - async def get_chat_completions(self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]) -> Any: + async def get_chat_completions( + self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam] + ) -> Any: """Get chat completions from SambaNova API endpoint.""" params = { - 'model': self.model_name, - 'stream': True, - 'messages': messages, - 'tools': context.tools, - 'tool_choice': context.tool_choice, - 'stream_options': {'include_usage': True}, - 'temperature': self._settings['temperature'], - 'top_p': self._settings['top_p'], - 'max_tokens': self._settings['max_tokens'], - 'max_completion_tokens': self._settings['max_completion_tokens'], + "model": self.model_name, + "stream": True, + "messages": messages, + "tools": context.tools, + "tool_choice": context.tool_choice, + "stream_options": {"include_usage": True}, + "temperature": self._settings["temperature"], + "top_p": self._settings["top_p"], + "max_tokens": self._settings["max_tokens"], + "max_completion_tokens": self._settings["max_completion_tokens"], } - params.update(self._settings['extra']) + params.update(self._settings["extra"]) chunks = await self._client.chat.completions.create(**params) return chunks @@ -78,13 +84,15 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore arguments_list = [] tool_id_list = [] func_idx = 0 - function_name = '' - arguments = '' - tool_call_id = '' + function_name = "" + arguments = "" + tool_call_id = "" await self.start_ttfb_metrics() - chunk_stream: AsyncStream[ChatCompletionChunk] = await self._stream_chat_completions(context) + chunk_stream: AsyncStream[ChatCompletionChunk] = await self._stream_chat_completions( + context + ) async for chunk in chunk_stream: if chunk.usage: @@ -120,9 +128,9 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore functions_list.append(function_name) arguments_list.append(arguments) tool_id_list.append(tool_call_id) - function_name = '' - arguments = '' - tool_call_id = '' + function_name = "" + arguments = "" + tool_call_id = "" func_idx += 1 if tool_call.function and tool_call.function.name: function_name += tool_call.function.name @@ -135,8 +143,10 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore # When gpt-4o-audio / gpt-4o-mini-audio is used for llm or stt+llm # we need to get LLMTextFrame for the transcript - elif hasattr(chunk.choices[0].delta, 'audio') and chunk.choices[0].delta.audio.get('transcript'): - await self.push_frame(LLMTextFrame(chunk.choices[0].delta.audio['transcript'])) + elif hasattr(chunk.choices[0].delta, "audio") and chunk.choices[0].delta.audio.get( + "transcript" + ): + await self.push_frame(LLMTextFrame(chunk.choices[0].delta.audio["transcript"])) # if we got a function name and arguments, check to see if it's a function with # a registered handler. If so, run the registered callback, save the result to @@ -150,7 +160,9 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore function_calls = [] - for function_name, arguments, tool_id in zip(functions_list, arguments_list, tool_id_list): + for function_name, arguments, tool_id in zip( + functions_list, arguments_list, tool_id_list + ): # This allows compatibility until SambaNova API introduces indexing in tool calls. if len(arguments) < 1: continue @@ -165,4 +177,4 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore ) ) - await self.run_function_calls(function_calls) \ No newline at end of file + await self.run_function_calls(function_calls) diff --git a/src/pipecat/services/sambanova/stt.py b/src/pipecat/services/sambanova/stt.py index 63520410e..ed518d6b8 100644 --- a/src/pipecat/services/sambanova/stt.py +++ b/src/pipecat/services/sambanova/stt.py @@ -27,9 +27,9 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore def __init__( self, *, - model: str = 'Whisper-Large-v3', + model: str = "Whisper-Large-v3", api_key: Optional[str] = None, - base_url: str = 'https://api.sambanova.ai/v1', + base_url: str = "https://api.sambanova.ai/v1", language: Optional[Language] = Language.EN, prompt: Optional[str] = None, temperature: Optional[float] = None, @@ -50,16 +50,16 @@ class SambaNovaSTTService(BaseWhisperSTTService): # type: ignore # Build kwargs dict with only set parameters kwargs = { - 'file': ('audio.wav', audio, 'audio/wav'), - 'model': self.model_name, - 'response_format': 'json', - 'language': self._language, + "file": ("audio.wav", audio, "audio/wav"), + "model": self.model_name, + "response_format": "json", + "language": self._language, } if self._prompt is not None: - kwargs['prompt'] = self._prompt + kwargs["prompt"] = self._prompt if self._temperature is not None: - kwargs['temperature'] = self._temperature + kwargs["temperature"] = self._temperature - return await self._client.audio.transcriptions.create(**kwargs) \ No newline at end of file + return await self._client.audio.transcriptions.create(**kwargs) From e26dbffcbe9cb14bb497db5b7da3a0da12550305 Mon Sep 17 00:00:00 2001 From: jhpiedrahitao Date: Mon, 23 Jun 2025 12:22:08 -0500 Subject: [PATCH 3/6] update sambanova init imports --- src/pipecat/services/sambanova/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pipecat/services/sambanova/__init__.py b/src/pipecat/services/sambanova/__init__.py index 8dbcb522a..5d8f7f797 100644 --- a/src/pipecat/services/sambanova/__init__.py +++ b/src/pipecat/services/sambanova/__init__.py @@ -4,11 +4,6 @@ # SPDX-License-Identifier: BSD 2-Clause License # -import sys - -from pipecat.services import DeprecatedModuleProxy - from .llm import * from .stt import * -sys.modules[__name__] = DeprecatedModuleProxy(globals(), "sambanova", "sambanova.[llm,stt,tts]") From a51280afa6efe436e0ba21778883574a2730dcb7 Mon Sep 17 00:00:00 2001 From: jhpiedrahitao Date: Mon, 23 Jun 2025 12:53:32 -0500 Subject: [PATCH 4/6] add 13 and 14 type foundational examples for sambanova iontegration --- .../13g-sambanova-transcription.py | 109 +++++++++++++ .../14s-function-calling-sambanova.py | 152 ++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 examples/foundational/13g-sambanova-transcription.py create mode 100644 examples/foundational/14s-function-calling-sambanova.py diff --git a/examples/foundational/13g-sambanova-transcription.py b/examples/foundational/13g-sambanova-transcription.py new file mode 100644 index 000000000..01feec7a1 --- /dev/null +++ b/examples/foundational/13g-sambanova-transcription.py @@ -0,0 +1,109 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import argparse +import time +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.audio.vad.vad_analyzer import VADParams +from pipecat.frames.frames import Frame, TranscriptionFrame, UserStoppedSpeakingFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.frame_processor import FrameDirection, FrameProcessor +from pipecat.services.sambanova.stt import SambaNovaSTTService +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams +from pipecat.transports.services.daily import DailyParams + +load_dotenv(override=True) + + +STOP_SECS = 2.0 + + +class TranscriptionLogger(FrameProcessor): + """Measures transcription latency. + + Uses the (intentionally) long STOP_SECS parameter to give the transcription time to finish, + then outputs the timing between when the VAD first classified audio input as not-speech and + the delivery of the last transcription frame. + """ + + def __init__(self): + super().__init__() + self._last_transcription_time = time.time() + + async def process_frame(self, frame: Frame, direction: FrameDirection): + await super().process_frame(frame, direction) + + if isinstance(frame, UserStoppedSpeakingFrame): + logger.debug( + f"Transcription latency: {(STOP_SECS - (time.time() - self._last_transcription_time)):.2f}" + ) + + if isinstance(frame, TranscriptionFrame): + self._last_transcription_time = time.time() + + +# We store functions so objects (e.g. SileroVADAnalyzer) don't get +# instantiated. The function will be called when the desired transport gets +# selected. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=STOP_SECS)), + ), +} + + +async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool): + logger.info(f"Starting bot") + + + stt = SambaNovaSTTService( + model='Whisper-Large-v3', + api_key=os.getenv('SAMBANOVA_API_KEY'), + ) + + tl = TranscriptionLogger() + + pipeline = Pipeline([transport.input(), stt, tl]) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + ) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=handle_sigint) + + await runner.run(task) + + +if __name__ == "__main__": + from pipecat.examples.run import main + + main(run_example, transport_params=transport_params) diff --git a/examples/foundational/14s-function-calling-sambanova.py b/examples/foundational/14s-function-calling-sambanova.py new file mode 100644 index 000000000..7c29e7110 --- /dev/null +++ b/examples/foundational/14s-function-calling-sambanova.py @@ -0,0 +1,152 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import argparse +import os + +from dotenv import load_dotenv +from loguru import logger + +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.audio.vad.silero import SileroVADAnalyzer +from pipecat.frames.frames import TTSSpeakFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_response import LLMUserAggregatorParams +from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext +from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.sambanova.llm import SambaNovaLLMService +from pipecat.services.sambanova.stt import SambaNovaSTTService +from pipecat.services.llm_service import FunctionCallParams +from pipecat.transports.base_transport import BaseTransport, TransportParams +from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams +from pipecat.transports.services.daily import DailyParams + +load_dotenv(override=True) + + +async def fetch_weather_from_api(params: FunctionCallParams): + await params.result_callback({"conditions": "nice", "temperature": "75"}) + + +# We store functions so objects (e.g. SileroVADAnalyzer) don't get +# instantiated. The function will be called when the desired transport gets +# selected. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + vad_analyzer=SileroVADAnalyzer(), + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + vad_analyzer=SileroVADAnalyzer(), + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + vad_analyzer=SileroVADAnalyzer(), + ), +} + + +async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool): + logger.info(f"Starting bot") + + stt = SambaNovaSTTService( + model='Whisper-Large-v3', + api_key=os.getenv('SAMBANOVA_API_KEY'), + ) + + tts = CartesiaTTSService( + api_key=os.getenv("CARTESIA_API_KEY"), + voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady + ) + + llm = SambaNovaLLMService( + api_key=os.getenv('SAMBANOVA_API_KEY'), + model='Llama-4-Maverick-17B-128E-Instruct', + ) + # You can also register a function_name of None to get all functions + # sent to the same callback with an additional function_name parameter. + llm.register_function("get_current_weather", fetch_weather_from_api) + + @llm.event_handler("on_function_calls_started") + async def on_function_calls_started(service, function_calls): + await tts.queue_frame(TTSSpeakFrame("Let me check on that.")) + + weather_function = FunctionSchema( + name="get_current_weather", + description="Get the current weather", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the user's location.", + }, + }, + required=["location"], + ) + tools = ToolsSchema(standard_tools=[weather_function]) + messages = [ + { + "role": "system", + "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.", + }, + ] + + context = OpenAILLMContext(messages, tools) + context_aggregator = llm.create_context_aggregator( + context, user_params=LLMUserAggregatorParams(aggregation_timeout=0.05) + ) + + pipeline = Pipeline( + [ + transport.input(), + stt, + context_aggregator.user(), + llm, + tts, + transport.output(), + context_aggregator.assistant(), + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + ), + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + # Kick off the conversation. + await task.queue_frames([context_aggregator.user().get_context_frame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=handle_sigint) + + await runner.run(task) + + +if __name__ == "__main__": + from pipecat.examples.run import main + + main(run_example, transport_params=transport_params) From d07f45132f7d2428fe7a3acad7909ae8374670fd Mon Sep 17 00:00:00 2001 From: jhpiedrahitao Date: Mon, 23 Jun 2025 12:54:00 -0500 Subject: [PATCH 5/6] update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a45172e2e..6351cdaa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `LLMAssistantContextAggregator` that exposes whether a function call is in progress. +- Added `SambaNovaLLMService` which provides llm api integration with an + OpenAI-compatible interface. + +- Added `SambaNovaTTSService` which provides speech-to-text functionality using + SambaNovas's (whisper) API. + +- Add fundational examples for function calling and transcription + `14s-function-calling-sambanova.py`, `13g-sambanova-transcription.py` + ### Changed - The `PipelineParams` arg `allow_interruptions` now defaults to `True`. From dfb0da32a938b2cdb8b5640b25554be49c6f4f34 Mon Sep 17 00:00:00 2001 From: jhpiedrahitao Date: Tue, 24 Jun 2025 15:59:40 -0500 Subject: [PATCH 6/6] fmt --- examples/foundational/13g-sambanova-transcription.py | 7 +++---- .../foundational/14s-function-calling-sambanova.py | 10 +++++----- src/pipecat/services/sambanova/__init__.py | 1 - 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/examples/foundational/13g-sambanova-transcription.py b/examples/foundational/13g-sambanova-transcription.py index 01feec7a1..bff8aa4d4 100644 --- a/examples/foundational/13g-sambanova-transcription.py +++ b/examples/foundational/13g-sambanova-transcription.py @@ -5,8 +5,8 @@ # import argparse -import time import os +import time from dotenv import load_dotenv from loguru import logger @@ -75,10 +75,9 @@ transport_params = { async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_sigint: bool): logger.info(f"Starting bot") - stt = SambaNovaSTTService( - model='Whisper-Large-v3', - api_key=os.getenv('SAMBANOVA_API_KEY'), + model="Whisper-Large-v3", + api_key=os.getenv("SAMBANOVA_API_KEY"), ) tl = TranscriptionLogger() diff --git a/examples/foundational/14s-function-calling-sambanova.py b/examples/foundational/14s-function-calling-sambanova.py index 7c29e7110..dd11af527 100644 --- a/examples/foundational/14s-function-calling-sambanova.py +++ b/examples/foundational/14s-function-calling-sambanova.py @@ -20,9 +20,9 @@ from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.llm_response import LLMUserAggregatorParams from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.cartesia.tts import CartesiaTTSService +from pipecat.services.llm_service import FunctionCallParams from pipecat.services.sambanova.llm import SambaNovaLLMService from pipecat.services.sambanova.stt import SambaNovaSTTService -from pipecat.services.llm_service import FunctionCallParams from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.network.fastapi_websocket import FastAPIWebsocketParams from pipecat.transports.services.daily import DailyParams @@ -60,8 +60,8 @@ async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_si logger.info(f"Starting bot") stt = SambaNovaSTTService( - model='Whisper-Large-v3', - api_key=os.getenv('SAMBANOVA_API_KEY'), + model="Whisper-Large-v3", + api_key=os.getenv("SAMBANOVA_API_KEY"), ) tts = CartesiaTTSService( @@ -70,8 +70,8 @@ async def run_example(transport: BaseTransport, _: argparse.Namespace, handle_si ) llm = SambaNovaLLMService( - api_key=os.getenv('SAMBANOVA_API_KEY'), - model='Llama-4-Maverick-17B-128E-Instruct', + api_key=os.getenv("SAMBANOVA_API_KEY"), + model="Llama-4-Maverick-17B-128E-Instruct", ) # You can also register a function_name of None to get all functions # sent to the same callback with an additional function_name parameter. diff --git a/src/pipecat/services/sambanova/__init__.py b/src/pipecat/services/sambanova/__init__.py index 5d8f7f797..828b755d2 100644 --- a/src/pipecat/services/sambanova/__init__.py +++ b/src/pipecat/services/sambanova/__init__.py @@ -6,4 +6,3 @@ from .llm import * from .stt import * -