dv-pipecat-ai 0.0.74.dev770__py3-none-any.whl → 0.0.82.dev776__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dv-pipecat-ai might be problematic. Click here for more details.
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
- dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
- pipecat/__init__.py +17 -0
- pipecat/adapters/base_llm_adapter.py +36 -1
- pipecat/adapters/schemas/direct_function.py +296 -0
- pipecat/adapters/schemas/function_schema.py +15 -6
- pipecat/adapters/schemas/tools_schema.py +55 -7
- pipecat/adapters/services/anthropic_adapter.py +22 -3
- pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
- pipecat/adapters/services/bedrock_adapter.py +22 -3
- pipecat/adapters/services/gemini_adapter.py +16 -3
- pipecat/adapters/services/open_ai_adapter.py +17 -2
- pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
- pipecat/audio/filters/base_audio_filter.py +30 -6
- pipecat/audio/filters/koala_filter.py +37 -2
- pipecat/audio/filters/krisp_filter.py +59 -6
- pipecat/audio/filters/noisereduce_filter.py +37 -0
- pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
- pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
- pipecat/audio/mixers/base_audio_mixer.py +30 -7
- pipecat/audio/mixers/soundfile_mixer.py +53 -6
- pipecat/audio/resamplers/base_audio_resampler.py +17 -9
- pipecat/audio/resamplers/resampy_resampler.py +26 -1
- pipecat/audio/resamplers/soxr_resampler.py +32 -1
- pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
- pipecat/audio/utils.py +194 -1
- pipecat/audio/vad/silero.py +60 -3
- pipecat/audio/vad/vad_analyzer.py +114 -30
- pipecat/clocks/base_clock.py +19 -0
- pipecat/clocks/system_clock.py +25 -0
- pipecat/extensions/voicemail/__init__.py +0 -0
- pipecat/extensions/voicemail/voicemail_detector.py +707 -0
- pipecat/frames/frames.py +590 -156
- pipecat/metrics/metrics.py +64 -1
- pipecat/observers/base_observer.py +58 -19
- pipecat/observers/loggers/debug_log_observer.py +56 -64
- pipecat/observers/loggers/llm_log_observer.py +8 -1
- pipecat/observers/loggers/transcription_log_observer.py +19 -7
- pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
- pipecat/observers/turn_tracking_observer.py +26 -1
- pipecat/pipeline/base_pipeline.py +5 -7
- pipecat/pipeline/base_task.py +52 -9
- pipecat/pipeline/parallel_pipeline.py +121 -177
- pipecat/pipeline/pipeline.py +129 -20
- pipecat/pipeline/runner.py +50 -1
- pipecat/pipeline/sync_parallel_pipeline.py +132 -32
- pipecat/pipeline/task.py +263 -280
- pipecat/pipeline/task_observer.py +85 -34
- pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
- pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
- pipecat/processors/aggregators/gated.py +25 -24
- pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
- pipecat/processors/aggregators/llm_response.py +398 -89
- pipecat/processors/aggregators/openai_llm_context.py +161 -13
- pipecat/processors/aggregators/sentence.py +25 -14
- pipecat/processors/aggregators/user_response.py +28 -3
- pipecat/processors/aggregators/vision_image_frame.py +24 -14
- pipecat/processors/async_generator.py +28 -0
- pipecat/processors/audio/audio_buffer_processor.py +78 -37
- pipecat/processors/consumer_processor.py +25 -6
- pipecat/processors/filters/frame_filter.py +23 -0
- pipecat/processors/filters/function_filter.py +30 -0
- pipecat/processors/filters/identity_filter.py +17 -2
- pipecat/processors/filters/null_filter.py +24 -1
- pipecat/processors/filters/stt_mute_filter.py +56 -21
- pipecat/processors/filters/wake_check_filter.py +46 -3
- pipecat/processors/filters/wake_notifier_filter.py +21 -3
- pipecat/processors/frame_processor.py +488 -131
- pipecat/processors/frameworks/langchain.py +38 -3
- pipecat/processors/frameworks/rtvi.py +719 -34
- pipecat/processors/gstreamer/pipeline_source.py +41 -0
- pipecat/processors/idle_frame_processor.py +26 -3
- pipecat/processors/logger.py +23 -0
- pipecat/processors/metrics/frame_processor_metrics.py +77 -4
- pipecat/processors/metrics/sentry.py +42 -4
- pipecat/processors/producer_processor.py +34 -14
- pipecat/processors/text_transformer.py +22 -10
- pipecat/processors/transcript_processor.py +48 -29
- pipecat/processors/user_idle_processor.py +31 -21
- pipecat/runner/__init__.py +1 -0
- pipecat/runner/daily.py +132 -0
- pipecat/runner/livekit.py +148 -0
- pipecat/runner/run.py +543 -0
- pipecat/runner/types.py +67 -0
- pipecat/runner/utils.py +515 -0
- pipecat/serializers/base_serializer.py +42 -0
- pipecat/serializers/exotel.py +17 -6
- pipecat/serializers/genesys.py +95 -0
- pipecat/serializers/livekit.py +33 -0
- pipecat/serializers/plivo.py +16 -15
- pipecat/serializers/protobuf.py +37 -1
- pipecat/serializers/telnyx.py +18 -17
- pipecat/serializers/twilio.py +32 -16
- pipecat/services/ai_service.py +5 -3
- pipecat/services/anthropic/llm.py +113 -43
- pipecat/services/assemblyai/models.py +63 -5
- pipecat/services/assemblyai/stt.py +64 -11
- pipecat/services/asyncai/__init__.py +0 -0
- pipecat/services/asyncai/tts.py +501 -0
- pipecat/services/aws/llm.py +185 -111
- pipecat/services/aws/stt.py +217 -23
- pipecat/services/aws/tts.py +118 -52
- pipecat/services/aws/utils.py +101 -5
- pipecat/services/aws_nova_sonic/aws.py +82 -64
- pipecat/services/aws_nova_sonic/context.py +15 -6
- pipecat/services/azure/common.py +10 -2
- pipecat/services/azure/image.py +32 -0
- pipecat/services/azure/llm.py +9 -7
- pipecat/services/azure/stt.py +65 -2
- pipecat/services/azure/tts.py +154 -23
- pipecat/services/cartesia/stt.py +125 -8
- pipecat/services/cartesia/tts.py +102 -38
- pipecat/services/cerebras/llm.py +15 -23
- pipecat/services/deepgram/stt.py +19 -11
- pipecat/services/deepgram/tts.py +36 -0
- pipecat/services/deepseek/llm.py +14 -23
- pipecat/services/elevenlabs/tts.py +330 -64
- pipecat/services/fal/image.py +43 -0
- pipecat/services/fal/stt.py +48 -10
- pipecat/services/fireworks/llm.py +14 -21
- pipecat/services/fish/tts.py +109 -9
- pipecat/services/gemini_multimodal_live/__init__.py +1 -0
- pipecat/services/gemini_multimodal_live/events.py +83 -2
- pipecat/services/gemini_multimodal_live/file_api.py +189 -0
- pipecat/services/gemini_multimodal_live/gemini.py +218 -21
- pipecat/services/gladia/config.py +17 -10
- pipecat/services/gladia/stt.py +82 -36
- pipecat/services/google/frames.py +40 -0
- pipecat/services/google/google.py +2 -0
- pipecat/services/google/image.py +39 -2
- pipecat/services/google/llm.py +176 -58
- pipecat/services/google/llm_openai.py +26 -4
- pipecat/services/google/llm_vertex.py +37 -15
- pipecat/services/google/rtvi.py +41 -0
- pipecat/services/google/stt.py +65 -17
- pipecat/services/google/test-google-chirp.py +45 -0
- pipecat/services/google/tts.py +390 -19
- pipecat/services/grok/llm.py +8 -6
- pipecat/services/groq/llm.py +8 -6
- pipecat/services/groq/stt.py +13 -9
- pipecat/services/groq/tts.py +40 -0
- pipecat/services/hamsa/__init__.py +9 -0
- pipecat/services/hamsa/stt.py +241 -0
- pipecat/services/heygen/__init__.py +5 -0
- pipecat/services/heygen/api.py +281 -0
- pipecat/services/heygen/client.py +620 -0
- pipecat/services/heygen/video.py +338 -0
- pipecat/services/image_service.py +5 -3
- pipecat/services/inworld/__init__.py +1 -0
- pipecat/services/inworld/tts.py +592 -0
- pipecat/services/llm_service.py +127 -45
- pipecat/services/lmnt/tts.py +80 -7
- pipecat/services/mcp_service.py +85 -44
- pipecat/services/mem0/memory.py +42 -13
- pipecat/services/minimax/tts.py +74 -15
- pipecat/services/mistral/__init__.py +0 -0
- pipecat/services/mistral/llm.py +185 -0
- pipecat/services/moondream/vision.py +55 -10
- pipecat/services/neuphonic/tts.py +275 -48
- pipecat/services/nim/llm.py +8 -6
- pipecat/services/ollama/llm.py +27 -7
- pipecat/services/openai/base_llm.py +54 -16
- pipecat/services/openai/image.py +30 -0
- pipecat/services/openai/llm.py +7 -5
- pipecat/services/openai/stt.py +13 -9
- pipecat/services/openai/tts.py +42 -10
- pipecat/services/openai_realtime_beta/azure.py +11 -9
- pipecat/services/openai_realtime_beta/context.py +7 -5
- pipecat/services/openai_realtime_beta/events.py +10 -7
- pipecat/services/openai_realtime_beta/openai.py +37 -18
- pipecat/services/openpipe/llm.py +30 -24
- pipecat/services/openrouter/llm.py +9 -7
- pipecat/services/perplexity/llm.py +15 -19
- pipecat/services/piper/tts.py +26 -12
- pipecat/services/playht/tts.py +227 -65
- pipecat/services/qwen/llm.py +8 -6
- pipecat/services/rime/tts.py +128 -17
- pipecat/services/riva/stt.py +160 -22
- pipecat/services/riva/tts.py +67 -2
- pipecat/services/sambanova/llm.py +19 -17
- pipecat/services/sambanova/stt.py +14 -8
- pipecat/services/sarvam/tts.py +60 -13
- pipecat/services/simli/video.py +82 -21
- pipecat/services/soniox/__init__.py +0 -0
- pipecat/services/soniox/stt.py +398 -0
- pipecat/services/speechmatics/stt.py +29 -17
- pipecat/services/stt_service.py +47 -11
- pipecat/services/tavus/video.py +94 -25
- pipecat/services/together/llm.py +8 -6
- pipecat/services/tts_service.py +77 -53
- pipecat/services/ultravox/stt.py +46 -43
- pipecat/services/vision_service.py +5 -3
- pipecat/services/websocket_service.py +12 -11
- pipecat/services/whisper/base_stt.py +58 -12
- pipecat/services/whisper/stt.py +69 -58
- pipecat/services/xtts/tts.py +59 -2
- pipecat/sync/base_notifier.py +19 -0
- pipecat/sync/event_notifier.py +24 -0
- pipecat/tests/utils.py +73 -5
- pipecat/transcriptions/language.py +24 -0
- pipecat/transports/base_input.py +112 -8
- pipecat/transports/base_output.py +235 -13
- pipecat/transports/base_transport.py +119 -0
- pipecat/transports/local/audio.py +76 -0
- pipecat/transports/local/tk.py +84 -0
- pipecat/transports/network/fastapi_websocket.py +174 -15
- pipecat/transports/network/small_webrtc.py +383 -39
- pipecat/transports/network/webrtc_connection.py +214 -8
- pipecat/transports/network/websocket_client.py +171 -1
- pipecat/transports/network/websocket_server.py +147 -9
- pipecat/transports/services/daily.py +792 -70
- pipecat/transports/services/helpers/daily_rest.py +122 -129
- pipecat/transports/services/livekit.py +339 -4
- pipecat/transports/services/tavus.py +273 -38
- pipecat/utils/asyncio/task_manager.py +92 -186
- pipecat/utils/base_object.py +83 -1
- pipecat/utils/network.py +2 -0
- pipecat/utils/string.py +114 -58
- pipecat/utils/text/base_text_aggregator.py +44 -13
- pipecat/utils/text/base_text_filter.py +46 -0
- pipecat/utils/text/markdown_text_filter.py +70 -14
- pipecat/utils/text/pattern_pair_aggregator.py +18 -14
- pipecat/utils/text/simple_text_aggregator.py +43 -2
- pipecat/utils/text/skip_tags_aggregator.py +21 -13
- pipecat/utils/time.py +36 -0
- pipecat/utils/tracing/class_decorators.py +32 -7
- pipecat/utils/tracing/conversation_context_provider.py +12 -2
- pipecat/utils/tracing/service_attributes.py +80 -64
- pipecat/utils/tracing/service_decorators.py +48 -21
- pipecat/utils/tracing/setup.py +13 -7
- pipecat/utils/tracing/turn_context_provider.py +12 -2
- pipecat/utils/tracing/turn_trace_observer.py +27 -0
- pipecat/utils/utils.py +14 -14
- dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
- pipecat/examples/daily_runner.py +0 -64
- pipecat/examples/run.py +0 -265
- pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
- pipecat/utils/asyncio/watchdog_event.py +0 -42
- pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
- pipecat/utils/asyncio/watchdog_queue.py +0 -48
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
- /pipecat/{examples → extensions}/__init__.py +0 -0
pipecat/runner/run.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024–2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Pipecat development runner.
|
|
8
|
+
|
|
9
|
+
This development runner executes Pipecat bots and provides the supporting
|
|
10
|
+
infrastructure they need - creating Daily rooms and tokens, managing WebRTC
|
|
11
|
+
connections, and setting up telephony webhook/WebSocket infrastructure. It
|
|
12
|
+
supports multiple transport types with a unified interface.
|
|
13
|
+
|
|
14
|
+
Install with::
|
|
15
|
+
|
|
16
|
+
pip install pipecat-ai[runner]
|
|
17
|
+
|
|
18
|
+
All bots must implement a `bot(runner_args)` async function as the entry point.
|
|
19
|
+
The server automatically discovers and executes this function when connections
|
|
20
|
+
are established.
|
|
21
|
+
|
|
22
|
+
Single transport example::
|
|
23
|
+
|
|
24
|
+
async def bot(runner_args: RunnerArguments):
|
|
25
|
+
transport = DailyTransport(
|
|
26
|
+
runner_args.room_url,
|
|
27
|
+
runner_args.token,
|
|
28
|
+
"Bot",
|
|
29
|
+
DailyParams(...)
|
|
30
|
+
)
|
|
31
|
+
# Your bot logic here
|
|
32
|
+
await run_pipeline(transport)
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
from pipecat.runner.run import main
|
|
36
|
+
main()
|
|
37
|
+
|
|
38
|
+
Multiple transport example::
|
|
39
|
+
|
|
40
|
+
async def bot(runner_args: RunnerArguments):
|
|
41
|
+
# Type-safe transport detection
|
|
42
|
+
if isinstance(runner_args, DailyRunnerArguments):
|
|
43
|
+
transport = setup_daily_transport(runner_args) # Your application code
|
|
44
|
+
elif isinstance(runner_args, SmallWebRTCRunnerArguments):
|
|
45
|
+
transport = setup_webrtc_transport(runner_args) # Your application code
|
|
46
|
+
elif isinstance(runner_args, WebSocketRunnerArguments):
|
|
47
|
+
transport = setup_telephony_transport(runner_args) # Your application code
|
|
48
|
+
|
|
49
|
+
# Your bot implementation
|
|
50
|
+
await run_pipeline(transport)
|
|
51
|
+
|
|
52
|
+
Supported transports:
|
|
53
|
+
|
|
54
|
+
- Daily - Creates rooms and tokens, runs bot as participant
|
|
55
|
+
- WebRTC - Provides local WebRTC interface with prebuilt UI
|
|
56
|
+
- Telephony - Handles webhook and WebSocket connections for Twilio, Telnyx, Plivo, Exotel
|
|
57
|
+
|
|
58
|
+
To run locally:
|
|
59
|
+
|
|
60
|
+
- WebRTC: `python bot.py -t webrtc`
|
|
61
|
+
- ESP32: `python bot.py -t webrtc --esp32 --host 192.168.1.100`
|
|
62
|
+
- Daily (server): `python bot.py -t daily`
|
|
63
|
+
- Daily (direct, testing only): `python bot.py -d`
|
|
64
|
+
- Telephony: `python bot.py -t twilio -x your_username.ngrok.io`
|
|
65
|
+
- Exotel: `python bot.py -t exotel` (no proxy needed, but ngrok connection to HTTP 7860 is required)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
import argparse
|
|
69
|
+
import asyncio
|
|
70
|
+
import os
|
|
71
|
+
import sys
|
|
72
|
+
from contextlib import asynccontextmanager
|
|
73
|
+
from typing import Dict
|
|
74
|
+
|
|
75
|
+
from loguru import logger
|
|
76
|
+
|
|
77
|
+
from pipecat.runner.types import (
|
|
78
|
+
DailyRunnerArguments,
|
|
79
|
+
SmallWebRTCRunnerArguments,
|
|
80
|
+
WebSocketRunnerArguments,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
import uvicorn
|
|
85
|
+
from dotenv import load_dotenv
|
|
86
|
+
from fastapi import BackgroundTasks, FastAPI, Request, WebSocket
|
|
87
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
88
|
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
89
|
+
except ImportError as e:
|
|
90
|
+
logger.error(f"Runner dependencies not available: {e}")
|
|
91
|
+
logger.error("To use Pipecat runners, install with: pip install pipecat-ai[runner]")
|
|
92
|
+
raise ImportError(
|
|
93
|
+
"Runner dependencies required. Install with: pip install pipecat-ai[runner]"
|
|
94
|
+
) from e
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
load_dotenv(override=True)
|
|
98
|
+
os.environ["ENV"] = "local"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_bot_module():
|
|
102
|
+
"""Get the bot module from the calling script."""
|
|
103
|
+
import importlib.util
|
|
104
|
+
|
|
105
|
+
# Get the main module (the file that was executed)
|
|
106
|
+
main_module = sys.modules["__main__"]
|
|
107
|
+
|
|
108
|
+
# Check if it has a bot function
|
|
109
|
+
if hasattr(main_module, "bot"):
|
|
110
|
+
return main_module
|
|
111
|
+
|
|
112
|
+
# Try to import 'bot' module from current directory
|
|
113
|
+
try:
|
|
114
|
+
import bot # type: ignore[import-untyped]
|
|
115
|
+
|
|
116
|
+
return bot
|
|
117
|
+
except ImportError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# Look for any .py file in current directory that has a bot function
|
|
121
|
+
# (excluding server.py).
|
|
122
|
+
cwd = os.getcwd()
|
|
123
|
+
for filename in os.listdir(cwd):
|
|
124
|
+
if filename.endswith(".py") and filename != "server.py":
|
|
125
|
+
try:
|
|
126
|
+
module_name = filename[:-3] # Remove .py extension
|
|
127
|
+
spec = importlib.util.spec_from_file_location(
|
|
128
|
+
module_name, os.path.join(cwd, filename)
|
|
129
|
+
)
|
|
130
|
+
module = importlib.util.module_from_spec(spec)
|
|
131
|
+
spec.loader.exec_module(module)
|
|
132
|
+
|
|
133
|
+
if hasattr(module, "bot"):
|
|
134
|
+
return module
|
|
135
|
+
except Exception:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
raise ImportError(
|
|
139
|
+
"Could not find 'bot' function. Make sure your bot file has a 'bot' function."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def _run_telephony_bot(websocket: WebSocket):
|
|
144
|
+
"""Run a bot for telephony transports."""
|
|
145
|
+
bot_module = _get_bot_module()
|
|
146
|
+
|
|
147
|
+
# Just pass the WebSocket - let the bot handle parsing
|
|
148
|
+
runner_args = WebSocketRunnerArguments(websocket=websocket)
|
|
149
|
+
|
|
150
|
+
await bot_module.bot(runner_args)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _create_server_app(
|
|
154
|
+
transport_type: str, host: str = "localhost", proxy: str = None, esp32_mode: bool = False
|
|
155
|
+
):
|
|
156
|
+
"""Create FastAPI app with transport-specific routes."""
|
|
157
|
+
app = FastAPI()
|
|
158
|
+
|
|
159
|
+
app.add_middleware(
|
|
160
|
+
CORSMiddleware,
|
|
161
|
+
allow_origins=["*"],
|
|
162
|
+
allow_credentials=True,
|
|
163
|
+
allow_methods=["*"],
|
|
164
|
+
allow_headers=["*"],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Set up transport-specific routes
|
|
168
|
+
if transport_type == "webrtc":
|
|
169
|
+
_setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host)
|
|
170
|
+
elif transport_type == "daily":
|
|
171
|
+
_setup_daily_routes(app)
|
|
172
|
+
elif transport_type in ["twilio", "telnyx", "plivo", "exotel"]:
|
|
173
|
+
_setup_telephony_routes(app, transport_type, proxy)
|
|
174
|
+
else:
|
|
175
|
+
logger.warning(f"Unknown transport type: {transport_type}")
|
|
176
|
+
|
|
177
|
+
return app
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _setup_webrtc_routes(app: FastAPI, esp32_mode: bool = False, host: str = "localhost"):
|
|
181
|
+
"""Set up WebRTC-specific routes."""
|
|
182
|
+
try:
|
|
183
|
+
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
|
184
|
+
|
|
185
|
+
from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
|
|
186
|
+
except ImportError as e:
|
|
187
|
+
logger.error(f"WebRTC transport dependencies not installed: {e}")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
# Store connections by pc_id
|
|
191
|
+
pcs_map: Dict[str, SmallWebRTCConnection] = {}
|
|
192
|
+
|
|
193
|
+
# Mount the frontend
|
|
194
|
+
app.mount("/client", SmallWebRTCPrebuiltUI)
|
|
195
|
+
|
|
196
|
+
@app.get("/", include_in_schema=False)
|
|
197
|
+
async def root_redirect():
|
|
198
|
+
"""Redirect root requests to client interface."""
|
|
199
|
+
return RedirectResponse(url="/client/")
|
|
200
|
+
|
|
201
|
+
@app.post("/api/offer")
|
|
202
|
+
async def offer(request: dict, background_tasks: BackgroundTasks):
|
|
203
|
+
"""Handle WebRTC offer requests and manage peer connections."""
|
|
204
|
+
pc_id = request.get("pc_id")
|
|
205
|
+
|
|
206
|
+
if pc_id and pc_id in pcs_map:
|
|
207
|
+
pipecat_connection = pcs_map[pc_id]
|
|
208
|
+
logger.info(f"Reusing existing connection for pc_id: {pc_id}")
|
|
209
|
+
await pipecat_connection.renegotiate(
|
|
210
|
+
sdp=request["sdp"],
|
|
211
|
+
type=request["type"],
|
|
212
|
+
restart_pc=request.get("restart_pc", False),
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
pipecat_connection = SmallWebRTCConnection()
|
|
216
|
+
await pipecat_connection.initialize(sdp=request["sdp"], type=request["type"])
|
|
217
|
+
|
|
218
|
+
@pipecat_connection.event_handler("closed")
|
|
219
|
+
async def handle_disconnected(webrtc_connection: SmallWebRTCConnection):
|
|
220
|
+
"""Handle WebRTC connection closure and cleanup."""
|
|
221
|
+
logger.info(f"Discarding peer connection for pc_id: {webrtc_connection.pc_id}")
|
|
222
|
+
pcs_map.pop(webrtc_connection.pc_id, None)
|
|
223
|
+
|
|
224
|
+
bot_module = _get_bot_module()
|
|
225
|
+
runner_args = SmallWebRTCRunnerArguments(webrtc_connection=pipecat_connection)
|
|
226
|
+
background_tasks.add_task(bot_module.bot, runner_args)
|
|
227
|
+
|
|
228
|
+
answer = pipecat_connection.get_answer()
|
|
229
|
+
|
|
230
|
+
# Apply ESP32 SDP munging if enabled
|
|
231
|
+
if esp32_mode and host != "localhost":
|
|
232
|
+
from pipecat.runner.utils import smallwebrtc_sdp_munging
|
|
233
|
+
|
|
234
|
+
answer["sdp"] = smallwebrtc_sdp_munging(answer["sdp"], host)
|
|
235
|
+
|
|
236
|
+
pcs_map[answer["pc_id"]] = pipecat_connection
|
|
237
|
+
return answer
|
|
238
|
+
|
|
239
|
+
@asynccontextmanager
|
|
240
|
+
async def lifespan(app: FastAPI):
|
|
241
|
+
"""Manage FastAPI application lifecycle and cleanup connections."""
|
|
242
|
+
yield
|
|
243
|
+
coros = [pc.disconnect() for pc in pcs_map.values()]
|
|
244
|
+
await asyncio.gather(*coros)
|
|
245
|
+
pcs_map.clear()
|
|
246
|
+
|
|
247
|
+
app.router.lifespan_context = lifespan
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _setup_daily_routes(app: FastAPI):
|
|
251
|
+
"""Set up Daily-specific routes."""
|
|
252
|
+
|
|
253
|
+
@app.get("/")
|
|
254
|
+
async def start_agent():
|
|
255
|
+
"""Launch a Daily bot and redirect to room."""
|
|
256
|
+
print("Starting bot with Daily transport")
|
|
257
|
+
|
|
258
|
+
import aiohttp
|
|
259
|
+
|
|
260
|
+
from pipecat.runner.daily import configure
|
|
261
|
+
|
|
262
|
+
async with aiohttp.ClientSession() as session:
|
|
263
|
+
room_url, token = await configure(session)
|
|
264
|
+
|
|
265
|
+
# Start the bot in the background with empty body for GET requests
|
|
266
|
+
bot_module = _get_bot_module()
|
|
267
|
+
runner_args = DailyRunnerArguments(room_url=room_url, token=token)
|
|
268
|
+
asyncio.create_task(bot_module.bot(runner_args))
|
|
269
|
+
return RedirectResponse(room_url)
|
|
270
|
+
|
|
271
|
+
async def _handle_rtvi_request(request: Request):
|
|
272
|
+
"""Common handler for both /start and /connect endpoints.
|
|
273
|
+
|
|
274
|
+
Expects POST body like::
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
"createDailyRoom": true,
|
|
278
|
+
"dailyRoomProperties": { "start_video_off": true },
|
|
279
|
+
"body": { "custom_data": "value" }
|
|
280
|
+
}
|
|
281
|
+
"""
|
|
282
|
+
print("Starting bot with Daily transport")
|
|
283
|
+
|
|
284
|
+
# Parse the request body
|
|
285
|
+
try:
|
|
286
|
+
request_data = await request.json()
|
|
287
|
+
logger.debug(f"Received request: {request_data}")
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"Failed to parse request body: {e}")
|
|
290
|
+
request_data = {}
|
|
291
|
+
|
|
292
|
+
# Extract the body data that should be passed to the bot
|
|
293
|
+
# This mimics Pipecat Cloud's behavior
|
|
294
|
+
bot_body = request_data.get("body", {})
|
|
295
|
+
|
|
296
|
+
# Log the extracted body data for debugging
|
|
297
|
+
if bot_body:
|
|
298
|
+
logger.info(f"Extracted body data for bot: {bot_body}")
|
|
299
|
+
else:
|
|
300
|
+
logger.debug("No body data provided in request")
|
|
301
|
+
|
|
302
|
+
import aiohttp
|
|
303
|
+
|
|
304
|
+
from pipecat.runner.daily import configure
|
|
305
|
+
|
|
306
|
+
async with aiohttp.ClientSession() as session:
|
|
307
|
+
room_url, token = await configure(session)
|
|
308
|
+
|
|
309
|
+
# Start the bot in the background with extracted body data
|
|
310
|
+
bot_module = _get_bot_module()
|
|
311
|
+
runner_args = DailyRunnerArguments(room_url=room_url, token=token, body=bot_body)
|
|
312
|
+
asyncio.create_task(bot_module.bot(runner_args))
|
|
313
|
+
# Match PCC /start endpoint response format:
|
|
314
|
+
return {"dailyRoom": room_url, "dailyToken": token}
|
|
315
|
+
|
|
316
|
+
@app.post("/start")
|
|
317
|
+
async def rtvi_start(request: Request):
|
|
318
|
+
"""Launch a Daily bot and return connection info for RTVI clients."""
|
|
319
|
+
return await _handle_rtvi_request(request)
|
|
320
|
+
|
|
321
|
+
@app.post("/connect")
|
|
322
|
+
async def rtvi_connect(request: Request):
|
|
323
|
+
"""Launch a Daily bot and return connection info for RTVI clients.
|
|
324
|
+
|
|
325
|
+
.. deprecated:: 0.0.78
|
|
326
|
+
Use /start instead. This endpoint will be removed in a future version.
|
|
327
|
+
"""
|
|
328
|
+
logger.warning(
|
|
329
|
+
"DEPRECATED: /connect endpoint is deprecated. Please use /start instead. "
|
|
330
|
+
"This endpoint will be removed in a future version."
|
|
331
|
+
)
|
|
332
|
+
return await _handle_rtvi_request(request)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _setup_telephony_routes(app: FastAPI, transport_type: str, proxy: str):
|
|
336
|
+
"""Set up telephony-specific routes."""
|
|
337
|
+
# XML response templates (Exotel doesn't use XML webhooks)
|
|
338
|
+
XML_TEMPLATES = {
|
|
339
|
+
"twilio": f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
340
|
+
<Response>
|
|
341
|
+
<Connect>
|
|
342
|
+
<Stream url="wss://{proxy}/ws"></Stream>
|
|
343
|
+
</Connect>
|
|
344
|
+
<Pause length="40"/>
|
|
345
|
+
</Response>""",
|
|
346
|
+
"telnyx": f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
347
|
+
<Response>
|
|
348
|
+
<Connect>
|
|
349
|
+
<Stream url="wss://{proxy}/ws" bidirectionalMode="rtp"></Stream>
|
|
350
|
+
</Connect>
|
|
351
|
+
<Pause length="40"/>
|
|
352
|
+
</Response>""",
|
|
353
|
+
"plivo": f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
354
|
+
<Response>
|
|
355
|
+
<Stream bidirectional="true" keepCallAlive="true" contentType="audio/x-mulaw;rate=8000">wss://{proxy}/ws</Stream>
|
|
356
|
+
</Response>""",
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@app.post("/")
|
|
360
|
+
async def start_call():
|
|
361
|
+
"""Handle telephony webhook and return XML response."""
|
|
362
|
+
if transport_type == "exotel":
|
|
363
|
+
# Exotel doesn't use POST webhooks - redirect to proper documentation
|
|
364
|
+
logger.debug("POST Exotel endpoint - not used")
|
|
365
|
+
return {
|
|
366
|
+
"error": "Exotel doesn't use POST webhooks",
|
|
367
|
+
"websocket_url": f"wss://{proxy}/ws",
|
|
368
|
+
"note": "Configure the WebSocket URL above in your Exotel App Bazaar Voicebot Applet",
|
|
369
|
+
}
|
|
370
|
+
else:
|
|
371
|
+
logger.debug(f"POST {transport_type.upper()} XML")
|
|
372
|
+
xml_content = XML_TEMPLATES.get(transport_type, "<Response></Response>")
|
|
373
|
+
return HTMLResponse(content=xml_content, media_type="application/xml")
|
|
374
|
+
|
|
375
|
+
@app.websocket("/ws")
|
|
376
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
377
|
+
"""Handle WebSocket connections for telephony."""
|
|
378
|
+
await websocket.accept()
|
|
379
|
+
logger.debug("WebSocket connection accepted")
|
|
380
|
+
await _run_telephony_bot(websocket)
|
|
381
|
+
|
|
382
|
+
@app.get("/")
|
|
383
|
+
async def start_agent():
|
|
384
|
+
"""Simple status endpoint for telephony transports."""
|
|
385
|
+
return {"status": f"Bot started with {transport_type}"}
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def _run_daily_direct():
|
|
389
|
+
"""Run Daily bot with direct connection (no FastAPI server)."""
|
|
390
|
+
try:
|
|
391
|
+
import aiohttp
|
|
392
|
+
|
|
393
|
+
from pipecat.runner.daily import configure
|
|
394
|
+
except ImportError as e:
|
|
395
|
+
logger.error("Daily transport dependencies not installed.")
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
logger.info("Running with direct Daily connection...")
|
|
399
|
+
|
|
400
|
+
async with aiohttp.ClientSession() as session:
|
|
401
|
+
room_url, token = await configure(session)
|
|
402
|
+
|
|
403
|
+
# Direct connections have no request body, so use empty dict
|
|
404
|
+
runner_args = DailyRunnerArguments(room_url=room_url, token=token)
|
|
405
|
+
runner_args.handle_sigint = True
|
|
406
|
+
|
|
407
|
+
# Get the bot module and run it directly
|
|
408
|
+
bot_module = _get_bot_module()
|
|
409
|
+
|
|
410
|
+
print(f"📞 Joining Daily room: {room_url}")
|
|
411
|
+
print(" (Direct connection - no web server needed)")
|
|
412
|
+
print()
|
|
413
|
+
|
|
414
|
+
await bot_module.bot(runner_args)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _validate_and_clean_proxy(proxy: str) -> str:
|
|
418
|
+
"""Validate and clean proxy hostname, removing protocol if present."""
|
|
419
|
+
if not proxy:
|
|
420
|
+
return proxy
|
|
421
|
+
|
|
422
|
+
original_proxy = proxy
|
|
423
|
+
|
|
424
|
+
# Strip common protocols
|
|
425
|
+
if proxy.startswith(("http://", "https://")):
|
|
426
|
+
proxy = proxy.split("://", 1)[1]
|
|
427
|
+
logger.warning(
|
|
428
|
+
f"Removed protocol from proxy URL. Using '{proxy}' instead of '{original_proxy}'. "
|
|
429
|
+
f"The --proxy argument expects only the hostname (e.g., 'mybot.ngrok.io')."
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Remove trailing slashes
|
|
433
|
+
proxy = proxy.rstrip("/")
|
|
434
|
+
|
|
435
|
+
return proxy
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def main():
|
|
439
|
+
"""Start the Pipecat development runner.
|
|
440
|
+
|
|
441
|
+
Parses command-line arguments and starts a FastAPI server configured
|
|
442
|
+
for the specified transport type. The runner will discover and run
|
|
443
|
+
any bot() function found in the current directory.
|
|
444
|
+
|
|
445
|
+
Command-line arguments:
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
--host: Server host address (default: localhost)
|
|
449
|
+
--port: Server port (default: 7860)
|
|
450
|
+
-t/--transport: Transport type (daily, webrtc, twilio, telnyx, plivo, exotel)
|
|
451
|
+
-x/--proxy: Public proxy hostname for telephony webhooks
|
|
452
|
+
--esp32: Enable SDP munging for ESP32 compatibility (requires --host with IP address)
|
|
453
|
+
-d/--direct: Connect directly to Daily room (automatically sets transport to daily)
|
|
454
|
+
-v/--verbose: Increase logging verbosity
|
|
455
|
+
|
|
456
|
+
The bot file must contain a `bot(runner_args)` function as the entry point.
|
|
457
|
+
"""
|
|
458
|
+
parser = argparse.ArgumentParser(description="Pipecat Development Runner")
|
|
459
|
+
parser.add_argument("--host", type=str, default="localhost", help="Host address")
|
|
460
|
+
parser.add_argument("--port", type=int, default=7860, help="Port number")
|
|
461
|
+
parser.add_argument(
|
|
462
|
+
"-t",
|
|
463
|
+
"--transport",
|
|
464
|
+
type=str,
|
|
465
|
+
choices=["daily", "webrtc", "twilio", "telnyx", "plivo", "exotel"],
|
|
466
|
+
default="webrtc",
|
|
467
|
+
help="Transport type",
|
|
468
|
+
)
|
|
469
|
+
parser.add_argument("--proxy", "-x", help="Public proxy host name")
|
|
470
|
+
parser.add_argument(
|
|
471
|
+
"--esp32",
|
|
472
|
+
action="store_true",
|
|
473
|
+
default=False,
|
|
474
|
+
help="Enable SDP munging for ESP32 compatibility (requires --host with IP address)",
|
|
475
|
+
)
|
|
476
|
+
parser.add_argument(
|
|
477
|
+
"-d",
|
|
478
|
+
"--direct",
|
|
479
|
+
action="store_true",
|
|
480
|
+
default=False,
|
|
481
|
+
help="Connect directly to Daily room (automatically sets transport to daily)",
|
|
482
|
+
)
|
|
483
|
+
parser.add_argument(
|
|
484
|
+
"--verbose", "-v", action="count", default=0, help="Increase logging verbosity"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
args = parser.parse_args()
|
|
488
|
+
|
|
489
|
+
# Validate and clean proxy hostname
|
|
490
|
+
if args.proxy:
|
|
491
|
+
args.proxy = _validate_and_clean_proxy(args.proxy)
|
|
492
|
+
|
|
493
|
+
# Auto-set transport to daily if --direct is used without explicit transport
|
|
494
|
+
if args.direct and args.transport == "webrtc": # webrtc is the default
|
|
495
|
+
args.transport = "daily"
|
|
496
|
+
elif args.direct and args.transport != "daily":
|
|
497
|
+
logger.error("--direct flag only works with Daily transport (-t daily)")
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
# Validate ESP32 requirements
|
|
501
|
+
if args.esp32 and args.host == "localhost":
|
|
502
|
+
logger.error("For ESP32, you need to specify `--host IP` so we can do SDP munging.")
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
# Log level
|
|
506
|
+
logger.remove()
|
|
507
|
+
logger.add(sys.stderr, level="TRACE" if args.verbose else "DEBUG")
|
|
508
|
+
|
|
509
|
+
# Handle direct Daily connection (no FastAPI server)
|
|
510
|
+
if args.direct:
|
|
511
|
+
print()
|
|
512
|
+
print("🚀 Connecting directly to Daily room...")
|
|
513
|
+
print()
|
|
514
|
+
|
|
515
|
+
# Run direct Daily connection
|
|
516
|
+
asyncio.run(_run_daily_direct())
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
# Print startup message for server-based transports
|
|
520
|
+
if args.transport == "webrtc":
|
|
521
|
+
print()
|
|
522
|
+
if args.esp32:
|
|
523
|
+
print(f"🚀 Bot ready! (ESP32 mode)")
|
|
524
|
+
print(f" → Open http://{args.host}:{args.port}/client in your browser")
|
|
525
|
+
else:
|
|
526
|
+
print(f"🚀 Bot ready!")
|
|
527
|
+
print(f" → Open http://{args.host}:{args.port}/client in your browser")
|
|
528
|
+
print()
|
|
529
|
+
elif args.transport == "daily":
|
|
530
|
+
print()
|
|
531
|
+
print(f"🚀 Bot ready!")
|
|
532
|
+
print(f" → Open http://{args.host}:{args.port} in your browser to start a session")
|
|
533
|
+
print()
|
|
534
|
+
|
|
535
|
+
# Create the app with transport-specific setup
|
|
536
|
+
app = _create_server_app(args.transport, args.host, args.proxy, args.esp32)
|
|
537
|
+
|
|
538
|
+
# Run the server
|
|
539
|
+
uvicorn.run(app, host=args.host, port=args.port)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
if __name__ == "__main__":
|
|
543
|
+
main()
|
pipecat/runner/types.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2024–2025, Daily
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: BSD 2-Clause License
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
"""Runner session argument types for the development runner.
|
|
8
|
+
|
|
9
|
+
These types are used by the development runner to pass transport-specific
|
|
10
|
+
information to bot functions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from fastapi import WebSocket
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RunnerArguments:
|
|
21
|
+
"""Base class for runner session arguments."""
|
|
22
|
+
|
|
23
|
+
handle_sigint: bool = field(init=False)
|
|
24
|
+
handle_sigterm: bool = field(init=False)
|
|
25
|
+
pipeline_idle_timeout_secs: int = field(init=False)
|
|
26
|
+
|
|
27
|
+
def __post_init__(self):
|
|
28
|
+
self.handle_sigint = False
|
|
29
|
+
self.handle_sigterm = False
|
|
30
|
+
self.pipeline_idle_timeout_secs = 300
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class DailyRunnerArguments(RunnerArguments):
|
|
35
|
+
"""Daily transport session arguments for the runner.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
room_url: Daily room URL to join
|
|
39
|
+
token: Authentication token for the room
|
|
40
|
+
body: Additional request data
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
room_url: str
|
|
44
|
+
token: Optional[str] = None
|
|
45
|
+
body: Optional[Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class WebSocketRunnerArguments(RunnerArguments):
|
|
50
|
+
"""WebSocket transport session arguments for the runner.
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
websocket: WebSocket connection for audio streaming
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
websocket: WebSocket
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class SmallWebRTCRunnerArguments(RunnerArguments):
|
|
61
|
+
"""Small WebRTC transport session arguments for the runner.
|
|
62
|
+
|
|
63
|
+
Parameters:
|
|
64
|
+
webrtc_connection: Pre-configured WebRTC peer connection
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
webrtc_connection: Any
|