Compare commits
22 Commits
hush/inter
...
jpt/smallw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5602f613dc | ||
|
|
89d2ef2bde | ||
|
|
f550015efb | ||
|
|
8fa44863fb | ||
|
|
088cb56922 | ||
|
|
a789e5feea | ||
|
|
16ca44131c | ||
|
|
418860cf26 | ||
|
|
e2fc8b3dce | ||
|
|
8b641089f8 | ||
|
|
d36ed755ce | ||
|
|
7aaf64fe55 | ||
|
|
5f52008974 | ||
|
|
d520677b23 | ||
|
|
42bd1e9d40 | ||
|
|
7f0494aa04 | ||
|
|
b7ae2989ac | ||
|
|
2b2b0f8121 | ||
|
|
5ca33a2b00 | ||
|
|
938dcb613d | ||
|
|
bc748cf9d0 | ||
|
|
3b55d16a49 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -5,6 +5,29 @@ All notable changes to **Pipecat** will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added `MistralLLMService`, using Mistral's chat completion API.
|
||||
|
||||
- For `OpenAILLMService` and its subclasses, added the ability to retry
|
||||
executing a chat completion after a timeout period. The new args are
|
||||
`retry_timeout_secs` and `retry_on_timeout`. This feature is disabled by
|
||||
default.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an `AudioBufferProcessor` issues that would cause audio overlap when
|
||||
setting a max buffer size.
|
||||
|
||||
- Fixed an issue where `AsyncAITTSService` had very high latency in responding
|
||||
by adding `force=true` when sending the flush command.
|
||||
|
||||
### Other
|
||||
|
||||
- Added `14w-function-calling-mistal.py` using `MistralLLMService`.
|
||||
|
||||
## [0.0.80] - 2025-08-13
|
||||
|
||||
### Added
|
||||
@@ -13,11 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
Gemini model can be prompted to insert styled speech to control the TTS
|
||||
output.
|
||||
|
||||
- For `OpenAILLMService` and its subclasses, added the ability to retry
|
||||
executing a chat completion after a timeout period. The new args are
|
||||
`retry_timeout_secs` and `retry_on_timeout`. This feature is disabled by
|
||||
default.
|
||||
|
||||
- Added Exotel support to Pipecat's development runner. You can now connect
|
||||
using the runner with `uv run bot.py -t exotel` and an ngrok connection to
|
||||
HTTP port 7860.
|
||||
|
||||
@@ -54,7 +54,7 @@ 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), [NVIDIA Riva](https://docs.pipecat.ai/server/services/stt/riva), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Soniox](https://docs.pipecat.ai/server/services/stt/soniox), [Speechmatics](https://docs.pipecat.ai/server/services/stt/speechmatics), [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) |
|
||||
| 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), [Mistral](https://docs.pipecat.ai/server/services/llm/mistral), [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 | [Async](https://docs.pipecat.ai/server/services/tts/asyncai), [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), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [Groq](https://docs.pipecat.ai/server/services/tts/groq), [Inworld](https://docs.pipecat.ai/server/services/tts/inworld), [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), [NVIDIA Riva](https://docs.pipecat.ai/server/services/tts/riva), [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 |
|
||||
|
||||
165
examples/foundational/14w-function-calling-mistral.py
Normal file
165
examples/foundational/14w-function-calling-mistral.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#
|
||||
# Copyright (c) 2024–2025, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
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.openai_llm_context import OpenAILLMContext
|
||||
from pipecat.runner.types import RunnerArguments
|
||||
from pipecat.runner.utils import create_transport
|
||||
from pipecat.services.cartesia.tts import CartesiaTTSService
|
||||
from pipecat.services.deepgram.stt import DeepgramSTTService
|
||||
from pipecat.services.llm_service import FunctionCallParams
|
||||
from pipecat.services.mistral.llm import MistralLLMService
|
||||
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"})
|
||||
|
||||
|
||||
async def fetch_restaurant_recommendation(params: FunctionCallParams):
|
||||
await params.result_callback({"name": "The Golden Dragon"})
|
||||
|
||||
|
||||
# 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_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||
logger.info(f"Starting bot")
|
||||
|
||||
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
|
||||
|
||||
tts = CartesiaTTSService(
|
||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||
voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
|
||||
)
|
||||
|
||||
llm = MistralLLMService(api_key=os.getenv("MISTRAL_API_KEY"))
|
||||
|
||||
# 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.register_function("get_restaurant_recommendation", fetch_restaurant_recommendation)
|
||||
|
||||
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", "format"],
|
||||
)
|
||||
restaurant_function = FunctionSchema(
|
||||
name="get_restaurant_recommendation",
|
||||
description="Get a restaurant recommendation",
|
||||
properties={
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
},
|
||||
},
|
||||
required=["location"],
|
||||
)
|
||||
tools = ToolsSchema(standard_tools=[weather_function, restaurant_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)
|
||||
|
||||
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,
|
||||
),
|
||||
idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
|
||||
)
|
||||
|
||||
@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=runner_args.handle_sigint)
|
||||
|
||||
await runner.run(task)
|
||||
|
||||
|
||||
async def bot(runner_args: RunnerArguments):
|
||||
"""Main bot entry point compatible with Pipecat Cloud."""
|
||||
transport = await create_transport(runner_args, transport_params)
|
||||
await run_bot(transport, runner_args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pipecat.runner.run import main
|
||||
|
||||
main()
|
||||
@@ -4,40 +4,32 @@ This directory contains examples showing how to build voice and multimodal agent
|
||||
|
||||
## Setup
|
||||
|
||||
1. Make sure you have uv installed:
|
||||
1. Follow the [README](../../README.md#%EF%B8%8F-contributing-to-the-framework) steps to get your local environment configured.
|
||||
|
||||
```bash
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
> **Run from root directory**: Make sure you are running the steps from the root directory.
|
||||
|
||||
> **Need help?** Refer to the [uv install documentation](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
> **Using local audio?**: The `LocalAudioTransport` requires a system dependency for `portaudio`. Install the dependency to use the transport.
|
||||
|
||||
2. Create a venv and install example dependencies:
|
||||
|
||||
```bash
|
||||
uv sync --all-extras --no-extra krisp
|
||||
```
|
||||
|
||||
3. Create a `.env` file with your API keys:
|
||||
2. Copy the [`env.example`](../../env.example) file and add API keys for services you plan to use:
|
||||
|
||||
```bash
|
||||
cp env.example .env
|
||||
# Edit .env with your API keys
|
||||
```
|
||||
|
||||
4. Navigate to the examples directory:
|
||||
3. Navigate to the examples directory if you aren't already there:
|
||||
|
||||
```bash
|
||||
cd examples/foundational
|
||||
```
|
||||
|
||||
5. Run any example:
|
||||
4. Run any example:
|
||||
|
||||
```bash
|
||||
uv run python 01-say-one-thing.py
|
||||
```
|
||||
|
||||
6. Open the web interface at http://localhost:7860/client/ and click "Connect"
|
||||
5. Open the web interface at http://localhost:7860/client/ and click "Connect"
|
||||
|
||||
## Running examples with other transports
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ lmnt = [ "websockets>=13.1,<15.0" ]
|
||||
local = [ "pyaudio~=0.2.14" ]
|
||||
mcp = [ "mcp[cli]~=1.9.4" ]
|
||||
mem0 = [ "mem0ai~=0.1.94" ]
|
||||
mistral = []
|
||||
mlx-whisper = [ "mlx-whisper~=0.4.2" ]
|
||||
moondream = [ "accelerate~=1.10.0", "einops~=0.8.0", "pyvips[binary]~=3.0.0", "timm~=1.0.13", "transformers>=4.48.0" ]
|
||||
nim = []
|
||||
|
||||
@@ -113,6 +113,7 @@ TESTS_14 = [
|
||||
("14q-function-calling-qwen.py", PROMPT_WEATHER, EVAL_WEATHER),
|
||||
("14r-function-calling-aws.py", PROMPT_WEATHER, EVAL_WEATHER),
|
||||
("14v-function-calling-openai.py", PROMPT_WEATHER, EVAL_WEATHER),
|
||||
("14w-function-calling-mistral.py", PROMPT_WEATHER, EVAL_WEATHER),
|
||||
# Currently not working.
|
||||
# ("14c-function-calling-together.py", PROMPT_WEATHER, EVAL_WEATHER),
|
||||
# ("14k-function-calling-cerebras.py", PROMPT_WEATHER, EVAL_WEATHER),
|
||||
|
||||
@@ -178,6 +178,7 @@ class AudioBufferProcessor(FrameProcessor):
|
||||
Calls audio handlers with any remaining buffered audio before stopping.
|
||||
"""
|
||||
await self._call_on_audio_data_handler()
|
||||
self._reset_recording()
|
||||
self._recording = False
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||
@@ -230,6 +231,7 @@ class AudioBufferProcessor(FrameProcessor):
|
||||
|
||||
if self._buffer_size > 0 and len(self._user_audio_buffer) > self._buffer_size:
|
||||
await self._call_on_audio_data_handler()
|
||||
self._reset_recording()
|
||||
|
||||
# Process turn recording with preprocessed data.
|
||||
if self._enable_turn_audio:
|
||||
@@ -288,8 +290,6 @@ class AudioBufferProcessor(FrameProcessor):
|
||||
self._num_channels,
|
||||
)
|
||||
|
||||
self._reset_audio_buffers()
|
||||
|
||||
def _buffer_has_audio(self, buffer: bytearray) -> bool:
|
||||
"""Check if a buffer contains audio data."""
|
||||
return buffer is not None and len(buffer) > 0
|
||||
|
||||
@@ -76,6 +76,7 @@ from loguru import logger
|
||||
|
||||
from pipecat.runner.types import (
|
||||
DailyRunnerArguments,
|
||||
SmallWebRTCPrebuiltArguments,
|
||||
SmallWebRTCRunnerArguments,
|
||||
WebSocketRunnerArguments,
|
||||
)
|
||||
@@ -151,7 +152,11 @@ async def _run_telephony_bot(websocket: WebSocket):
|
||||
|
||||
|
||||
def _create_server_app(
|
||||
transport_type: str, host: str = "localhost", proxy: str = None, esp32_mode: bool = False
|
||||
transport_type: str,
|
||||
host: str = "localhost",
|
||||
proxy: str = None,
|
||||
esp32_mode: bool = False,
|
||||
prebuilt_ui_config: SmallWebRTCPrebuiltArguments = None,
|
||||
):
|
||||
"""Create FastAPI app with transport-specific routes."""
|
||||
app = FastAPI()
|
||||
@@ -166,9 +171,9 @@ def _create_server_app(
|
||||
|
||||
# Set up transport-specific routes
|
||||
if transport_type == "webrtc":
|
||||
_setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host)
|
||||
_setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host, prebuilt_ui_config=prebuilt_ui_config)
|
||||
elif transport_type == "daily":
|
||||
_setup_daily_routes(app)
|
||||
_setup_daily_routes(app, prebuilt_ui_config=prebuilt_ui_config)
|
||||
elif transport_type in ["twilio", "telnyx", "plivo", "exotel"]:
|
||||
_setup_telephony_routes(app, transport_type, proxy)
|
||||
else:
|
||||
@@ -177,7 +182,12 @@ def _create_server_app(
|
||||
return app
|
||||
|
||||
|
||||
def _setup_webrtc_routes(app: FastAPI, esp32_mode: bool = False, host: str = "localhost"):
|
||||
def _setup_webrtc_routes(
|
||||
app: FastAPI,
|
||||
esp32_mode: bool = False,
|
||||
host: str = "localhost",
|
||||
prebuilt_ui_config: SmallWebRTCPrebuiltArguments = None,
|
||||
):
|
||||
"""Set up WebRTC-specific routes."""
|
||||
try:
|
||||
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
||||
@@ -191,7 +201,7 @@ def _setup_webrtc_routes(app: FastAPI, esp32_mode: bool = False, host: str = "lo
|
||||
pcs_map: Dict[str, SmallWebRTCConnection] = {}
|
||||
|
||||
# Mount the frontend
|
||||
app.mount("/client", SmallWebRTCPrebuiltUI)
|
||||
app.mount("/client", SmallWebRTCPrebuiltUI(config=prebuilt_ui_config))
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def root_redirect():
|
||||
@@ -402,6 +412,7 @@ async def _run_daily_direct():
|
||||
|
||||
# Direct connections have no request body, so use empty dict
|
||||
runner_args = DailyRunnerArguments(room_url=room_url, token=token)
|
||||
runner_args.handle_sigint = True
|
||||
|
||||
# Get the bot module and run it directly
|
||||
bot_module = _get_bot_module()
|
||||
@@ -434,7 +445,7 @@ def _validate_and_clean_proxy(proxy: str) -> str:
|
||||
return proxy
|
||||
|
||||
|
||||
def main():
|
||||
def main(prebuilt_ui_config: SmallWebRTCPrebuiltArguments = None):
|
||||
"""Start the Pipecat development runner.
|
||||
|
||||
Parses command-line arguments and starts a FastAPI server configured
|
||||
@@ -532,7 +543,7 @@ def main():
|
||||
print()
|
||||
|
||||
# Create the app with transport-specific setup
|
||||
app = _create_server_app(args.transport, args.host, args.proxy, args.esp32)
|
||||
app = _create_server_app(args.transport, args.host, args.proxy, args.esp32, prebuilt_ui_config)
|
||||
|
||||
# Run the server
|
||||
uvicorn.run(app, host=args.host, port=args.port)
|
||||
|
||||
@@ -65,3 +65,67 @@ class SmallWebRTCRunnerArguments(RunnerArguments):
|
||||
"""
|
||||
|
||||
webrtc_connection: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class SmallWebRTCPrebuiltArguments:
|
||||
"""Arguments for Small WebRTC prebuilt console template.
|
||||
|
||||
Parameters:
|
||||
no_rtvi: Disables RTVI related functionality. Default: False
|
||||
server_rtvi_version: Specifies the RTVI version in use by the server. Default: None
|
||||
no_user_audio: Disables user audio input entirely. Default: False
|
||||
no_user_video: Disables user video input entirely. Default: False
|
||||
user_video_enabled: Enables user video input. Default: False
|
||||
user_audio_enabled: Enables user audio input. Default: True
|
||||
no_audio_output: Disables audio output for the bot. Default: False
|
||||
no_bot_audio: Disables audio visualization for the bot. Default: False
|
||||
no_bot_video: Disables video visualization for the bot. Default: True
|
||||
transport_type: Type of transport to use for the RTVI client. Default: "smallwebrtc"
|
||||
transport_options: Options for configuring the transport. Default: None
|
||||
connect_params: Parameters for connecting to the transport. Default: None
|
||||
client_options: Options for configuring the RTVI client. Default: None
|
||||
theme: Theme to use for the UI. Default: "system"
|
||||
no_theme_switch: Disables the theme switcher in the header. Default: False
|
||||
no_logo: Disables the logo in the header. Default: False
|
||||
no_session_info: Disables the session info panel. Default: False
|
||||
no_status_info: Disables the status info panel. Default: False
|
||||
title_text: Title displayed in the header. Default: "Pipecat Playground"
|
||||
assistant_label_text: Label for assistant messages. Default: "assistant"
|
||||
user_label_text: Label for user messages. Default: "user"
|
||||
system_label_text: Label for system messages. Default: "system"
|
||||
collapse_info_panel: Whether to collapse the info panel by default. Default: False
|
||||
collapse_media_panel: Whether to collapse the media panel by default. Default: False
|
||||
"""
|
||||
|
||||
# RTVI configuration
|
||||
no_rtvi: bool = False
|
||||
server_rtvi_version: Optional[str] = None
|
||||
|
||||
# Media configuration
|
||||
no_user_audio: bool = False
|
||||
no_user_video: bool = False
|
||||
user_video_enabled: bool = False
|
||||
user_audio_enabled: bool = True
|
||||
no_audio_output: bool = False
|
||||
no_bot_audio: bool = False
|
||||
no_bot_video: bool = True
|
||||
|
||||
# Client & transport configuration
|
||||
transport_type: str = "smallwebrtc"
|
||||
transport_options: Optional[Any] = None
|
||||
connect_params: Optional[Any] = None
|
||||
client_options: Optional[Any] = None
|
||||
|
||||
# UI configuration
|
||||
theme: str = "system"
|
||||
no_theme_switch: bool = False
|
||||
no_logo: bool = False
|
||||
no_session_info: bool = False
|
||||
no_status_info: bool = False
|
||||
title_text: str = "Pipecat Playground"
|
||||
assistant_label_text: str = "assistant"
|
||||
user_label_text: str = "user"
|
||||
system_label_text: str = "system"
|
||||
collapse_info_panel: bool = False
|
||||
collapse_media_panel: bool = False
|
||||
|
||||
@@ -335,7 +335,7 @@ class AsyncAITTSService(InterruptibleTTSService):
|
||||
yield TTSStartedFrame()
|
||||
self._started = True
|
||||
|
||||
msg = self._build_msg(text=text)
|
||||
msg = self._build_msg(text=text, force=True)
|
||||
|
||||
try:
|
||||
await self._get_websocket().send(msg)
|
||||
|
||||
@@ -495,6 +495,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
|
||||
if self._context_id:
|
||||
await self._websocket.send(json.dumps({"close_socket": True}))
|
||||
await self._websocket.close()
|
||||
logger.debug("Disconnected from ElevenLabs")
|
||||
except Exception as e:
|
||||
logger.error(f"{self} error closing websocket: {e}")
|
||||
finally:
|
||||
|
||||
@@ -248,7 +248,7 @@ class HeyGenClient:
|
||||
if event_type == "agent.state":
|
||||
logger.debug(f"HeyGenClient ws received agent status: {event}")
|
||||
else:
|
||||
logger.error(f"HeyGenClient ws received unknown event: {event_type}")
|
||||
logger.trace(f"HeyGenClient ws received unknown event: {event_type}")
|
||||
|
||||
async def _ws_disconnect(self) -> None:
|
||||
"""Disconnect from HeyGen websocket endpoint."""
|
||||
|
||||
0
src/pipecat/services/mistral/__init__.py
Normal file
0
src/pipecat/services/mistral/__init__.py
Normal file
182
src/pipecat/services/mistral/llm.py
Normal file
182
src/pipecat/services/mistral/llm.py
Normal file
@@ -0,0 +1,182 @@
|
||||
#
|
||||
# Copyright (c) 2024–2025, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""Mistral LLM service implementation using OpenAI-compatible interface."""
|
||||
|
||||
from typing import List, Sequence
|
||||
|
||||
from loguru import logger
|
||||
from openai import AsyncStream
|
||||
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam
|
||||
|
||||
from pipecat.frames.frames import FunctionCallFromLLM
|
||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||
from pipecat.services.openai.llm import OpenAILLMService
|
||||
|
||||
|
||||
class MistralLLMService(OpenAILLMService):
|
||||
"""A service for interacting with Mistral's API using the OpenAI-compatible interface.
|
||||
|
||||
This service extends OpenAILLMService to connect to Mistral's API endpoint while
|
||||
maintaining full compatibility with OpenAI's interface and functionality.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str,
|
||||
base_url: str = "https://api.mistral.ai/v1",
|
||||
model: str = "mistral-small-latest",
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the Mistral LLM service.
|
||||
|
||||
Args:
|
||||
api_key: The API key for accessing Mistral's API.
|
||||
base_url: The base URL for Mistral API. Defaults to "https://api.mistral.ai/v1".
|
||||
model: The model identifier to use. Defaults to "mistral-small-latest".
|
||||
**kwargs: Additional keyword arguments passed to OpenAILLMService.
|
||||
"""
|
||||
super().__init__(api_key=api_key, base_url=base_url, model=model, **kwargs)
|
||||
|
||||
def create_client(self, api_key=None, base_url=None, **kwargs):
|
||||
"""Create OpenAI-compatible client for Mistral API endpoint.
|
||||
|
||||
Args:
|
||||
api_key: The API key for authentication. If None, uses instance key.
|
||||
base_url: The base URL for the API. If None, uses instance URL.
|
||||
**kwargs: Additional arguments passed to the client constructor.
|
||||
|
||||
Returns:
|
||||
An OpenAI-compatible client configured for Mistral API.
|
||||
"""
|
||||
logger.debug(f"Creating Mistral client with api {base_url}")
|
||||
return super().create_client(api_key, base_url, **kwargs)
|
||||
|
||||
def _apply_mistral_assistant_prefix(
|
||||
self, messages: List[ChatCompletionMessageParam]
|
||||
) -> List[ChatCompletionMessageParam]:
|
||||
"""Apply Mistral's assistant message prefix requirement.
|
||||
|
||||
Mistral requires assistant messages to have prefix=True when they
|
||||
are the final message in a conversation. According to Mistral's API:
|
||||
- Assistant messages with prefix=True MUST be the last message
|
||||
- Only add prefix=True to the final assistant message when needed
|
||||
- This allows assistant messages to be accepted as the last message
|
||||
|
||||
Args:
|
||||
messages: The original list of messages.
|
||||
|
||||
Returns:
|
||||
Messages with Mistral prefix requirement applied to final assistant message.
|
||||
"""
|
||||
if not messages:
|
||||
return messages
|
||||
|
||||
# Create a copy to avoid modifying the original
|
||||
fixed_messages = [dict(msg) for msg in messages]
|
||||
|
||||
# Get the last message
|
||||
last_message = fixed_messages[-1]
|
||||
|
||||
# Only add prefix=True to the last message if it's an assistant message
|
||||
# and Mistral would otherwise reject it
|
||||
if last_message.get("role") == "assistant" and "prefix" not in last_message:
|
||||
last_message["prefix"] = True
|
||||
|
||||
return fixed_messages
|
||||
|
||||
async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]):
|
||||
"""Execute function calls, filtering out already-completed ones.
|
||||
|
||||
Mistral and OpenAI have different function call detection patterns:
|
||||
|
||||
OpenAI (Stream-based detection):
|
||||
- Detects function calls only from streaming chunks as the LLM generates them
|
||||
- Second LLM completion doesn't re-detect existing tool_calls in message history
|
||||
- Function calls execute exactly once
|
||||
|
||||
Mistral (Message-based detection):
|
||||
- Detects function calls from the complete message history on each completion
|
||||
- Second LLM completion with the response re-detects the same tool_calls from
|
||||
previous messages
|
||||
- Without filtering, function calls would execute twice
|
||||
|
||||
This method prevents duplicate execution by:
|
||||
1. Checking message history for existing tool result messages
|
||||
2. Filtering out function calls that already have corresponding results
|
||||
3. Only executing function calls that haven't been completed yet
|
||||
|
||||
Note: This filtering prevents duplicate function execution, but the
|
||||
on_function_calls_started event may still fire twice due to the detection
|
||||
pattern difference. This is expected behavior.
|
||||
|
||||
Args:
|
||||
function_calls: The function calls to potentially execute.
|
||||
"""
|
||||
if not function_calls:
|
||||
return
|
||||
|
||||
# Filter out function calls that already have results
|
||||
calls_to_execute = []
|
||||
|
||||
# Get messages from the first function call's context (they should all have the same context)
|
||||
messages = function_calls[0].context.get_messages() if function_calls else []
|
||||
|
||||
# Get all tool_call_ids that already have results
|
||||
executed_call_ids = set()
|
||||
for msg in messages:
|
||||
if msg.get("role") == "tool" and msg.get("tool_call_id"):
|
||||
executed_call_ids.add(msg.get("tool_call_id"))
|
||||
|
||||
# Only include function calls that haven't been executed yet
|
||||
for call in function_calls:
|
||||
if call.tool_call_id not in executed_call_ids:
|
||||
calls_to_execute.append(call)
|
||||
else:
|
||||
logger.trace(
|
||||
f"Skipping already-executed function call: {call.function_name}:{call.tool_call_id}"
|
||||
)
|
||||
|
||||
# Call parent method with filtered list
|
||||
if calls_to_execute:
|
||||
await super().run_function_calls(calls_to_execute)
|
||||
|
||||
def build_chat_completion_params(
|
||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||
) -> dict:
|
||||
"""Build parameters for Mistral chat completion request.
|
||||
|
||||
Handles Mistral-specific requirements including:
|
||||
- Assistant message prefix requirement for API compatibility
|
||||
- Parameter mapping (random_seed instead of seed)
|
||||
- Core completion settings
|
||||
"""
|
||||
# Apply Mistral's assistant prefix requirement for API compatibility
|
||||
fixed_messages = self._apply_mistral_assistant_prefix(messages)
|
||||
|
||||
params = {
|
||||
"model": self.model_name,
|
||||
"stream": True,
|
||||
"messages": fixed_messages,
|
||||
"tools": context.tools,
|
||||
"tool_choice": context.tool_choice,
|
||||
"frequency_penalty": self._settings["frequency_penalty"],
|
||||
"presence_penalty": self._settings["presence_penalty"],
|
||||
"temperature": self._settings["temperature"],
|
||||
"top_p": self._settings["top_p"],
|
||||
"max_tokens": self._settings["max_tokens"],
|
||||
}
|
||||
|
||||
# Handle Mistral-specific parameter mapping
|
||||
# Mistral uses "random_seed" instead of "seed"
|
||||
if self._settings["seed"]:
|
||||
params["random_seed"] = self._settings["seed"]
|
||||
|
||||
# Add any extra parameters
|
||||
params.update(self._settings["extra"])
|
||||
|
||||
return params
|
||||
Reference in New Issue
Block a user