dv-pipecat-ai 0.0.85.dev699__py3-none-any.whl → 0.0.85.dev814__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.85.dev699.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/METADATA +23 -18
- {dv_pipecat_ai-0.0.85.dev699.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/RECORD +43 -43
- pipecat/adapters/services/aws_nova_sonic_adapter.py +116 -6
- pipecat/pipeline/runner.py +6 -2
- pipecat/pipeline/task.py +40 -55
- pipecat/processors/aggregators/llm_context.py +40 -2
- pipecat/processors/frameworks/rtvi.py +1 -0
- pipecat/runner/daily.py +59 -20
- pipecat/runner/run.py +149 -67
- pipecat/runner/types.py +5 -5
- pipecat/services/assemblyai/models.py +6 -0
- pipecat/services/assemblyai/stt.py +13 -5
- pipecat/services/asyncai/tts.py +3 -0
- pipecat/services/aws/llm.py +33 -16
- pipecat/services/aws/nova_sonic/context.py +69 -0
- pipecat/services/aws/nova_sonic/llm.py +199 -89
- pipecat/services/aws/stt.py +2 -0
- pipecat/services/aws_nova_sonic/context.py +8 -12
- pipecat/services/cartesia/stt.py +77 -70
- pipecat/services/cartesia/tts.py +3 -1
- pipecat/services/deepgram/flux/stt.py +4 -0
- pipecat/services/elevenlabs/tts.py +82 -41
- pipecat/services/fish/tts.py +3 -0
- pipecat/services/google/stt.py +4 -0
- pipecat/services/lmnt/tts.py +2 -0
- pipecat/services/neuphonic/tts.py +3 -0
- pipecat/services/openai/tts.py +37 -6
- pipecat/services/piper/tts.py +7 -9
- pipecat/services/playht/tts.py +3 -0
- pipecat/services/rime/tts.py +9 -8
- pipecat/services/riva/stt.py +3 -1
- pipecat/services/sarvam/tts.py +87 -10
- pipecat/services/speechmatics/stt.py +3 -1
- pipecat/services/stt_service.py +23 -10
- pipecat/services/tts_service.py +64 -13
- pipecat/transports/base_input.py +3 -0
- pipecat/transports/base_output.py +71 -77
- pipecat/transports/smallwebrtc/connection.py +5 -0
- pipecat/transports/smallwebrtc/request_handler.py +42 -0
- pipecat/utils/string.py +1 -0
- {dv_pipecat_ai-0.0.85.dev699.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.85.dev699.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.85.dev699.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/top_level.txt +0 -0
pipecat/runner/run.py
CHANGED
|
@@ -70,16 +70,19 @@ import asyncio
|
|
|
70
70
|
import mimetypes
|
|
71
71
|
import os
|
|
72
72
|
import sys
|
|
73
|
+
import uuid
|
|
73
74
|
from contextlib import asynccontextmanager
|
|
75
|
+
from http import HTTPMethod
|
|
74
76
|
from pathlib import Path
|
|
75
|
-
from typing import Optional
|
|
77
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
76
78
|
|
|
77
79
|
import aiohttp
|
|
78
|
-
from fastapi.responses import FileResponse
|
|
80
|
+
from fastapi.responses import FileResponse, Response
|
|
79
81
|
from loguru import logger
|
|
80
82
|
|
|
81
83
|
from pipecat.runner.types import (
|
|
82
84
|
DailyRunnerArguments,
|
|
85
|
+
RunnerArguments,
|
|
83
86
|
SmallWebRTCRunnerArguments,
|
|
84
87
|
WebSocketRunnerArguments,
|
|
85
88
|
)
|
|
@@ -166,6 +169,7 @@ def _create_server_app(
|
|
|
166
169
|
host: str = "localhost",
|
|
167
170
|
proxy: str,
|
|
168
171
|
esp32_mode: bool = False,
|
|
172
|
+
whatsapp_enabled: bool = False,
|
|
169
173
|
folder: Optional[str] = None,
|
|
170
174
|
):
|
|
171
175
|
"""Create FastAPI app with transport-specific routes."""
|
|
@@ -182,7 +186,8 @@ def _create_server_app(
|
|
|
182
186
|
# Set up transport-specific routes
|
|
183
187
|
if transport_type == "webrtc":
|
|
184
188
|
_setup_webrtc_routes(app, esp32_mode=esp32_mode, host=host, folder=folder)
|
|
185
|
-
|
|
189
|
+
if whatsapp_enabled:
|
|
190
|
+
_setup_whatsapp_routes(app)
|
|
186
191
|
elif transport_type == "daily":
|
|
187
192
|
_setup_daily_routes(app)
|
|
188
193
|
elif transport_type in TELEPHONY_TRANSPORTS:
|
|
@@ -200,8 +205,10 @@ def _setup_webrtc_routes(
|
|
|
200
205
|
try:
|
|
201
206
|
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
|
202
207
|
|
|
203
|
-
from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
|
|
208
|
+
from pipecat.transports.smallwebrtc.connection import IceServer, SmallWebRTCConnection
|
|
204
209
|
from pipecat.transports.smallwebrtc.request_handler import (
|
|
210
|
+
IceCandidate,
|
|
211
|
+
SmallWebRTCPatchRequest,
|
|
205
212
|
SmallWebRTCRequest,
|
|
206
213
|
SmallWebRTCRequestHandler,
|
|
207
214
|
)
|
|
@@ -209,6 +216,16 @@ def _setup_webrtc_routes(
|
|
|
209
216
|
logger.error(f"WebRTC transport dependencies not installed: {e}")
|
|
210
217
|
return
|
|
211
218
|
|
|
219
|
+
class IceConfig(TypedDict):
|
|
220
|
+
iceServers: List[IceServer]
|
|
221
|
+
|
|
222
|
+
class StartBotResult(TypedDict, total=False):
|
|
223
|
+
sessionId: str
|
|
224
|
+
iceConfig: Optional[IceConfig]
|
|
225
|
+
|
|
226
|
+
# In-memory store of active sessions: session_id -> session info
|
|
227
|
+
active_sessions: Dict[str, Dict[str, Any]] = {}
|
|
228
|
+
|
|
212
229
|
# Mount the frontend
|
|
213
230
|
app.mount("/client", SmallWebRTCPrebuiltUI)
|
|
214
231
|
|
|
@@ -217,7 +234,7 @@ def _setup_webrtc_routes(
|
|
|
217
234
|
"""Redirect root requests to client interface."""
|
|
218
235
|
return RedirectResponse(url="/client/")
|
|
219
236
|
|
|
220
|
-
@app.get("/files/{filename}")
|
|
237
|
+
@app.get("/files/{filename:path}")
|
|
221
238
|
async def download_file(filename: str):
|
|
222
239
|
"""Handle file downloads."""
|
|
223
240
|
if not folder:
|
|
@@ -254,6 +271,74 @@ def _setup_webrtc_routes(
|
|
|
254
271
|
)
|
|
255
272
|
return answer
|
|
256
273
|
|
|
274
|
+
@app.patch("/api/offer")
|
|
275
|
+
async def ice_candidate(request: SmallWebRTCPatchRequest):
|
|
276
|
+
"""Handle WebRTC new ice candidate requests."""
|
|
277
|
+
logger.debug(f"Received patch request: {request}")
|
|
278
|
+
await small_webrtc_handler.handle_patch_request(request)
|
|
279
|
+
return {"status": "success"}
|
|
280
|
+
|
|
281
|
+
@app.post("/start")
|
|
282
|
+
async def rtvi_start(request: Request):
|
|
283
|
+
"""Mimic Pipecat Cloud's /start endpoint."""
|
|
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
|
+
# Store session info immediately in memory, replicate the behavior expected on Pipecat Cloud
|
|
293
|
+
session_id = str(uuid.uuid4())
|
|
294
|
+
active_sessions[session_id] = request_data
|
|
295
|
+
|
|
296
|
+
result: StartBotResult = {"sessionId": session_id}
|
|
297
|
+
if request_data.get("enableDefaultIceServers"):
|
|
298
|
+
result["iceConfig"] = IceConfig(
|
|
299
|
+
iceServers=[IceServer(urls="stun:stun.l.google.com:19302")]
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
|
|
304
|
+
@app.api_route(
|
|
305
|
+
"/sessions/{session_id}/{path:path}",
|
|
306
|
+
methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
307
|
+
)
|
|
308
|
+
async def proxy_request(
|
|
309
|
+
session_id: str, path: str, request: Request, background_tasks: BackgroundTasks
|
|
310
|
+
):
|
|
311
|
+
"""Mimic Pipecat Cloud's proxy."""
|
|
312
|
+
active_session = active_sessions.get(session_id)
|
|
313
|
+
if active_session is None:
|
|
314
|
+
return Response(content="Invalid or not-yet-ready session_id", status_code=404)
|
|
315
|
+
|
|
316
|
+
if path.endswith("api/offer"):
|
|
317
|
+
# Parse the request body and convert to SmallWebRTCRequest
|
|
318
|
+
try:
|
|
319
|
+
request_data = await request.json()
|
|
320
|
+
if request.method == HTTPMethod.POST.value:
|
|
321
|
+
webrtc_request = SmallWebRTCRequest(
|
|
322
|
+
sdp=request_data["sdp"],
|
|
323
|
+
type=request_data["type"],
|
|
324
|
+
pc_id=request_data.get("pc_id"),
|
|
325
|
+
restart_pc=request_data.get("restart_pc"),
|
|
326
|
+
request_data=request_data,
|
|
327
|
+
)
|
|
328
|
+
return await offer(webrtc_request, background_tasks)
|
|
329
|
+
elif request.method == HTTPMethod.PATCH.value:
|
|
330
|
+
patch_request = SmallWebRTCPatchRequest(
|
|
331
|
+
pc_id=request_data["pc_id"],
|
|
332
|
+
candidates=[IceCandidate(**c) for c in request_data.get("candidates", [])],
|
|
333
|
+
)
|
|
334
|
+
return await ice_candidate(patch_request)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.error(f"Failed to parse WebRTC request: {e}")
|
|
337
|
+
return Response(content="Invalid WebRTC request", status_code=400)
|
|
338
|
+
|
|
339
|
+
logger.info(f"Received request for path: {path}")
|
|
340
|
+
return Response(status_code=200)
|
|
341
|
+
|
|
257
342
|
@asynccontextmanager
|
|
258
343
|
async def smallwebrtc_lifespan(app: FastAPI):
|
|
259
344
|
"""Manage FastAPI application lifecycle and cleanup connections."""
|
|
@@ -289,6 +374,29 @@ def _add_lifespan_to_app(app: FastAPI, new_lifespan):
|
|
|
289
374
|
|
|
290
375
|
def _setup_whatsapp_routes(app: FastAPI):
|
|
291
376
|
"""Set up WebRTC-specific routes."""
|
|
377
|
+
WHATSAPP_APP_SECRET = os.getenv("WHATSAPP_APP_SECRET")
|
|
378
|
+
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
|
|
379
|
+
WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN")
|
|
380
|
+
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN = os.getenv("WHATSAPP_WEBHOOK_VERIFICATION_TOKEN")
|
|
381
|
+
|
|
382
|
+
if not all(
|
|
383
|
+
[
|
|
384
|
+
WHATSAPP_APP_SECRET,
|
|
385
|
+
WHATSAPP_PHONE_NUMBER_ID,
|
|
386
|
+
WHATSAPP_TOKEN,
|
|
387
|
+
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN,
|
|
388
|
+
]
|
|
389
|
+
):
|
|
390
|
+
logger.error(
|
|
391
|
+
"""Missing required environment variables for WhatsApp transport:
|
|
392
|
+
WHATSAPP_APP_SECRET
|
|
393
|
+
WHATSAPP_PHONE_NUMBER_ID
|
|
394
|
+
WHATSAPP_TOKEN
|
|
395
|
+
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN
|
|
396
|
+
"""
|
|
397
|
+
)
|
|
398
|
+
return
|
|
399
|
+
|
|
292
400
|
try:
|
|
293
401
|
from pipecat_ai_small_webrtc_prebuilt.frontend import SmallWebRTCPrebuiltUI
|
|
294
402
|
|
|
@@ -300,24 +408,7 @@ def _setup_whatsapp_routes(app: FastAPI):
|
|
|
300
408
|
from pipecat.transports.whatsapp.api import WhatsAppWebhookRequest
|
|
301
409
|
from pipecat.transports.whatsapp.client import WhatsAppClient
|
|
302
410
|
except ImportError as e:
|
|
303
|
-
logger.error(f"
|
|
304
|
-
return
|
|
305
|
-
|
|
306
|
-
WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN")
|
|
307
|
-
WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID")
|
|
308
|
-
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN = os.getenv("WHATSAPP_WEBHOOK_VERIFICATION_TOKEN")
|
|
309
|
-
WHATSAPP_APP_SECRET = os.getenv("WHATSAPP_APP_SECRET")
|
|
310
|
-
|
|
311
|
-
if not all(
|
|
312
|
-
[
|
|
313
|
-
WHATSAPP_TOKEN,
|
|
314
|
-
WHATSAPP_PHONE_NUMBER_ID,
|
|
315
|
-
WHATSAPP_WEBHOOK_VERIFICATION_TOKEN,
|
|
316
|
-
]
|
|
317
|
-
):
|
|
318
|
-
logger.debug(
|
|
319
|
-
"Missing required environment variables for WhatsApp transport. Keeping it disabled."
|
|
320
|
-
)
|
|
411
|
+
logger.error(f"WhatsApp transport dependencies not installed: {e}")
|
|
321
412
|
return
|
|
322
413
|
|
|
323
414
|
# Global WhatsApp client instance
|
|
@@ -439,9 +530,9 @@ def _setup_daily_routes(app: FastAPI):
|
|
|
439
530
|
"""Set up Daily-specific routes."""
|
|
440
531
|
|
|
441
532
|
@app.get("/")
|
|
442
|
-
async def
|
|
533
|
+
async def create_room_and_start_agent():
|
|
443
534
|
"""Launch a Daily bot and redirect to room."""
|
|
444
|
-
print("Starting bot with Daily transport")
|
|
535
|
+
print("Starting bot with Daily transport and redirecting to Daily room")
|
|
445
536
|
|
|
446
537
|
import aiohttp
|
|
447
538
|
|
|
@@ -456,11 +547,11 @@ def _setup_daily_routes(app: FastAPI):
|
|
|
456
547
|
asyncio.create_task(bot_module.bot(runner_args))
|
|
457
548
|
return RedirectResponse(room_url)
|
|
458
549
|
|
|
459
|
-
|
|
460
|
-
|
|
550
|
+
@app.post("/start")
|
|
551
|
+
async def start_agent(request: Request):
|
|
552
|
+
"""Handler for /start endpoints.
|
|
461
553
|
|
|
462
554
|
Expects POST body like::
|
|
463
|
-
|
|
464
555
|
{
|
|
465
556
|
"createDailyRoom": true,
|
|
466
557
|
"dailyRoomProperties": { "start_video_off": true },
|
|
@@ -477,47 +568,32 @@ def _setup_daily_routes(app: FastAPI):
|
|
|
477
568
|
logger.error(f"Failed to parse request body: {e}")
|
|
478
569
|
request_data = {}
|
|
479
570
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
bot_body = request_data.get("body", {})
|
|
483
|
-
|
|
484
|
-
# Log the extracted body data for debugging
|
|
485
|
-
if bot_body:
|
|
486
|
-
logger.info(f"Extracted body data for bot: {bot_body}")
|
|
487
|
-
else:
|
|
488
|
-
logger.debug("No body data provided in request")
|
|
571
|
+
create_daily_room = request_data.get("createDailyRoom", False)
|
|
572
|
+
body = request_data.get("body", {})
|
|
489
573
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
from pipecat.runner.daily import configure
|
|
574
|
+
bot_module = _get_bot_module()
|
|
493
575
|
|
|
494
|
-
|
|
495
|
-
|
|
576
|
+
result = None
|
|
577
|
+
if create_daily_room:
|
|
578
|
+
import aiohttp
|
|
496
579
|
|
|
497
|
-
|
|
498
|
-
bot_module = _get_bot_module()
|
|
499
|
-
runner_args = DailyRunnerArguments(room_url=room_url, token=token, body=bot_body)
|
|
500
|
-
asyncio.create_task(bot_module.bot(runner_args))
|
|
501
|
-
# Match PCC /start endpoint response format:
|
|
502
|
-
return {"dailyRoom": room_url, "dailyToken": token}
|
|
580
|
+
from pipecat.runner.daily import configure
|
|
503
581
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
582
|
+
async with aiohttp.ClientSession() as session:
|
|
583
|
+
room_url, token = await configure(session)
|
|
584
|
+
runner_args = DailyRunnerArguments(room_url=room_url, token=token, body=body)
|
|
585
|
+
result = {
|
|
586
|
+
"dailyRoom": room_url,
|
|
587
|
+
"dailyToken": token,
|
|
588
|
+
"sessionId": str(uuid.uuid4()),
|
|
589
|
+
}
|
|
590
|
+
else:
|
|
591
|
+
runner_args = RunnerArguments(body=body)
|
|
508
592
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
"""Launch a Daily bot and return connection info for RTVI clients.
|
|
593
|
+
# Start the bot in the background
|
|
594
|
+
asyncio.create_task(bot_module.bot(runner_args))
|
|
512
595
|
|
|
513
|
-
|
|
514
|
-
Use /start instead. This endpoint will be removed in a future version.
|
|
515
|
-
"""
|
|
516
|
-
logger.warning(
|
|
517
|
-
"DEPRECATED: /connect endpoint is deprecated. Please use /start instead. "
|
|
518
|
-
"This endpoint will be removed in a future version."
|
|
519
|
-
)
|
|
520
|
-
return await _handle_rtvi_request(request)
|
|
596
|
+
return result
|
|
521
597
|
|
|
522
598
|
|
|
523
599
|
def _setup_telephony_routes(app: FastAPI, *, transport_type: str, proxy: str):
|
|
@@ -576,8 +652,6 @@ def _setup_telephony_routes(app: FastAPI, *, transport_type: str, proxy: str):
|
|
|
576
652
|
async def _run_daily_direct():
|
|
577
653
|
"""Run Daily bot with direct connection (no FastAPI server)."""
|
|
578
654
|
try:
|
|
579
|
-
import aiohttp
|
|
580
|
-
|
|
581
655
|
from pipecat.runner.daily import configure
|
|
582
656
|
except ImportError as e:
|
|
583
657
|
logger.error("Daily transport dependencies not installed.")
|
|
@@ -689,6 +763,12 @@ def main():
|
|
|
689
763
|
parser.add_argument(
|
|
690
764
|
"--verbose", "-v", action="count", default=0, help="Increase logging verbosity"
|
|
691
765
|
)
|
|
766
|
+
parser.add_argument(
|
|
767
|
+
"--whatsapp",
|
|
768
|
+
action="store_true",
|
|
769
|
+
default=False,
|
|
770
|
+
help="Ensure requried WhatsApp environment variables are present",
|
|
771
|
+
)
|
|
692
772
|
|
|
693
773
|
args = parser.parse_args()
|
|
694
774
|
|
|
@@ -731,10 +811,11 @@ def main():
|
|
|
731
811
|
print()
|
|
732
812
|
if args.esp32:
|
|
733
813
|
print(f"🚀 Bot ready! (ESP32 mode)")
|
|
734
|
-
|
|
814
|
+
elif args.whatsapp:
|
|
815
|
+
print(f"🚀 Bot ready! (WhatsApp)")
|
|
735
816
|
else:
|
|
736
817
|
print(f"🚀 Bot ready!")
|
|
737
|
-
|
|
818
|
+
print(f" → Open http://{args.host}:{args.port}/client in your browser")
|
|
738
819
|
print()
|
|
739
820
|
elif args.transport == "daily":
|
|
740
821
|
print()
|
|
@@ -752,6 +833,7 @@ def main():
|
|
|
752
833
|
host=args.host,
|
|
753
834
|
proxy=args.proxy,
|
|
754
835
|
esp32_mode=args.esp32,
|
|
836
|
+
whatsapp_enabled=args.whatsapp,
|
|
755
837
|
folder=args.folder,
|
|
756
838
|
)
|
|
757
839
|
|
pipecat/runner/types.py
CHANGED
|
@@ -20,9 +20,11 @@ from fastapi import WebSocket
|
|
|
20
20
|
class RunnerArguments:
|
|
21
21
|
"""Base class for runner session arguments."""
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
# Use kw_only so subclasses don't need to worry about ordering.
|
|
24
|
+
handle_sigint: bool = field(init=False, kw_only=True)
|
|
25
|
+
handle_sigterm: bool = field(init=False, kw_only=True)
|
|
26
|
+
pipeline_idle_timeout_secs: int = field(init=False, kw_only=True)
|
|
27
|
+
body: Optional[Any] = field(default_factory=dict, kw_only=True)
|
|
26
28
|
|
|
27
29
|
def __post_init__(self):
|
|
28
30
|
self.handle_sigint = False
|
|
@@ -42,7 +44,6 @@ class DailyRunnerArguments(RunnerArguments):
|
|
|
42
44
|
|
|
43
45
|
room_url: str
|
|
44
46
|
token: Optional[str] = None
|
|
45
|
-
body: Optional[Any] = field(default_factory=dict)
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
@dataclass
|
|
@@ -55,7 +56,6 @@ class WebSocketRunnerArguments(RunnerArguments):
|
|
|
55
56
|
"""
|
|
56
57
|
|
|
57
58
|
websocket: WebSocket
|
|
58
|
-
body: Optional[Any] = field(default_factory=dict)
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
@dataclass
|
|
@@ -108,6 +108,8 @@ class AssemblyAIConnectionParams(BaseModel):
|
|
|
108
108
|
end_of_turn_confidence_threshold: Confidence threshold for end-of-turn detection.
|
|
109
109
|
min_end_of_turn_silence_when_confident: Minimum silence duration when confident about end-of-turn.
|
|
110
110
|
max_turn_silence: Maximum silence duration before forcing end-of-turn.
|
|
111
|
+
keyterms_prompt: List of key terms to guide transcription. Will be JSON serialized before sending.
|
|
112
|
+
speech_model: Select between English and multilingual models. Defaults to "universal-streaming-english".
|
|
111
113
|
"""
|
|
112
114
|
|
|
113
115
|
sample_rate: int = 16000
|
|
@@ -117,3 +119,7 @@ class AssemblyAIConnectionParams(BaseModel):
|
|
|
117
119
|
end_of_turn_confidence_threshold: Optional[float] = None
|
|
118
120
|
min_end_of_turn_silence_when_confident: Optional[int] = None
|
|
119
121
|
max_turn_silence: Optional[int] = None
|
|
122
|
+
keyterms_prompt: Optional[List[str]] = None
|
|
123
|
+
speech_model: Literal["universal-streaming-english", "universal-streaming-multilingual"] = (
|
|
124
|
+
"universal-streaming-english"
|
|
125
|
+
)
|
|
@@ -174,11 +174,16 @@ class AssemblyAISTTService(STTService):
|
|
|
174
174
|
|
|
175
175
|
def _build_ws_url(self) -> str:
|
|
176
176
|
"""Build WebSocket URL with query parameters using urllib.parse.urlencode."""
|
|
177
|
-
params = {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
params = {}
|
|
178
|
+
for k, v in self._connection_params.model_dump().items():
|
|
179
|
+
if v is not None:
|
|
180
|
+
if k == "keyterms_prompt":
|
|
181
|
+
params[k] = json.dumps(v)
|
|
182
|
+
elif isinstance(v, bool):
|
|
183
|
+
params[k] = str(v).lower()
|
|
184
|
+
else:
|
|
185
|
+
params[k] = v
|
|
186
|
+
|
|
182
187
|
if params:
|
|
183
188
|
query_string = urlencode(params)
|
|
184
189
|
return f"{self._api_endpoint_base_url}?{query_string}"
|
|
@@ -197,6 +202,8 @@ class AssemblyAISTTService(STTService):
|
|
|
197
202
|
)
|
|
198
203
|
self._connected = True
|
|
199
204
|
self._receive_task = self.create_task(self._receive_task_handler())
|
|
205
|
+
|
|
206
|
+
await self._call_event_handler("on_connected")
|
|
200
207
|
except Exception as e:
|
|
201
208
|
logger.error(f"Failed to connect to AssemblyAI: {e}")
|
|
202
209
|
self._connected = False
|
|
@@ -238,6 +245,7 @@ class AssemblyAISTTService(STTService):
|
|
|
238
245
|
self._websocket = None
|
|
239
246
|
self._connected = False
|
|
240
247
|
self._receive_task = None
|
|
248
|
+
await self._call_event_handler("on_disconnected")
|
|
241
249
|
|
|
242
250
|
async def _receive_task_handler(self):
|
|
243
251
|
"""Handle incoming WebSocket messages."""
|
pipecat/services/asyncai/tts.py
CHANGED
|
@@ -235,6 +235,8 @@ class AsyncAITTSService(InterruptibleTTSService):
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
await self._get_websocket().send(json.dumps(init_msg))
|
|
238
|
+
|
|
239
|
+
await self._call_event_handler("on_connected")
|
|
238
240
|
except Exception as e:
|
|
239
241
|
logger.error(f"{self} initialization error: {e}")
|
|
240
242
|
self._websocket = None
|
|
@@ -252,6 +254,7 @@ class AsyncAITTSService(InterruptibleTTSService):
|
|
|
252
254
|
finally:
|
|
253
255
|
self._websocket = None
|
|
254
256
|
self._started = False
|
|
257
|
+
await self._call_event_handler("on_disconnected")
|
|
255
258
|
|
|
256
259
|
def _get_websocket(self):
|
|
257
260
|
if self._websocket:
|
pipecat/services/aws/llm.py
CHANGED
|
@@ -720,11 +720,11 @@ class AWSBedrockLLMService(LLMService):
|
|
|
720
720
|
additional_model_request_fields: Additional model-specific parameters.
|
|
721
721
|
"""
|
|
722
722
|
|
|
723
|
-
max_tokens: Optional[int] = Field(
|
|
724
|
-
temperature: Optional[float] = Field(
|
|
725
|
-
top_p: Optional[float] = Field(
|
|
723
|
+
max_tokens: Optional[int] = Field(default=None, ge=1)
|
|
724
|
+
temperature: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
725
|
+
top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
726
726
|
stop_sequences: Optional[List[str]] = Field(default_factory=lambda: [])
|
|
727
|
-
latency: Optional[str] = Field(
|
|
727
|
+
latency: Optional[str] = Field(default=None)
|
|
728
728
|
additional_model_request_fields: Optional[Dict[str, Any]] = Field(default_factory=dict)
|
|
729
729
|
|
|
730
730
|
def __init__(
|
|
@@ -801,6 +801,24 @@ class AWSBedrockLLMService(LLMService):
|
|
|
801
801
|
"""
|
|
802
802
|
return True
|
|
803
803
|
|
|
804
|
+
def _build_inference_config(self) -> Dict[str, Any]:
|
|
805
|
+
"""Build inference config with only the parameters that are set.
|
|
806
|
+
|
|
807
|
+
This prevents conflicts with models (e.g., Claude Sonnet 4.5) that don't
|
|
808
|
+
allow certain parameter combinations like temperature and top_p together.
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
Dictionary containing only the inference parameters that are not None.
|
|
812
|
+
"""
|
|
813
|
+
inference_config = {}
|
|
814
|
+
if self._settings["max_tokens"] is not None:
|
|
815
|
+
inference_config["maxTokens"] = self._settings["max_tokens"]
|
|
816
|
+
if self._settings["temperature"] is not None:
|
|
817
|
+
inference_config["temperature"] = self._settings["temperature"]
|
|
818
|
+
if self._settings["top_p"] is not None:
|
|
819
|
+
inference_config["topP"] = self._settings["top_p"]
|
|
820
|
+
return inference_config
|
|
821
|
+
|
|
804
822
|
async def run_inference(self, context: LLMContext | OpenAILLMContext) -> Optional[str]:
|
|
805
823
|
"""Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context.
|
|
806
824
|
|
|
@@ -826,16 +844,16 @@ class AWSBedrockLLMService(LLMService):
|
|
|
826
844
|
model_id = self.model_name
|
|
827
845
|
|
|
828
846
|
# Prepare request parameters
|
|
847
|
+
inference_config = self._build_inference_config()
|
|
848
|
+
|
|
829
849
|
request_params = {
|
|
830
850
|
"modelId": model_id,
|
|
831
851
|
"messages": messages,
|
|
832
|
-
"inferenceConfig": {
|
|
833
|
-
"maxTokens": 8192,
|
|
834
|
-
"temperature": 0.7,
|
|
835
|
-
"topP": 0.9,
|
|
836
|
-
},
|
|
837
852
|
}
|
|
838
853
|
|
|
854
|
+
if inference_config:
|
|
855
|
+
request_params["inferenceConfig"] = inference_config
|
|
856
|
+
|
|
839
857
|
if system:
|
|
840
858
|
request_params["system"] = system
|
|
841
859
|
|
|
@@ -974,21 +992,20 @@ class AWSBedrockLLMService(LLMService):
|
|
|
974
992
|
tools = params_from_context["tools"]
|
|
975
993
|
tool_choice = params_from_context["tool_choice"]
|
|
976
994
|
|
|
977
|
-
# Set up inference config
|
|
978
|
-
inference_config =
|
|
979
|
-
"maxTokens": self._settings["max_tokens"],
|
|
980
|
-
"temperature": self._settings["temperature"],
|
|
981
|
-
"topP": self._settings["top_p"],
|
|
982
|
-
}
|
|
995
|
+
# Set up inference config - only include parameters that are set
|
|
996
|
+
inference_config = self._build_inference_config()
|
|
983
997
|
|
|
984
998
|
# Prepare request parameters
|
|
985
999
|
request_params = {
|
|
986
1000
|
"modelId": self.model_name,
|
|
987
1001
|
"messages": messages,
|
|
988
|
-
"inferenceConfig": inference_config,
|
|
989
1002
|
"additionalModelRequestFields": self._settings["additional_model_request_fields"],
|
|
990
1003
|
}
|
|
991
1004
|
|
|
1005
|
+
# Only add inference config if it has parameters
|
|
1006
|
+
if inference_config:
|
|
1007
|
+
request_params["inferenceConfig"] = inference_config
|
|
1008
|
+
|
|
992
1009
|
# Add system message
|
|
993
1010
|
if system:
|
|
994
1011
|
request_params["system"] = system
|
|
@@ -8,8 +8,77 @@
|
|
|
8
8
|
|
|
9
9
|
This module provides specialized context aggregators and message handling for AWS Nova Sonic,
|
|
10
10
|
including conversation history management and role-specific message processing.
|
|
11
|
+
|
|
12
|
+
.. deprecated:: 0.0.91
|
|
13
|
+
AWS Nova Sonic no longer uses types from this module under the hood.
|
|
14
|
+
It now uses `LLMContext` and `LLMContextAggregatorPair`.
|
|
15
|
+
Using the new patterns should allow you to not need types from this module.
|
|
16
|
+
|
|
17
|
+
BEFORE:
|
|
18
|
+
```
|
|
19
|
+
# Setup
|
|
20
|
+
context = OpenAILLMContext(messages, tools)
|
|
21
|
+
context_aggregator = llm.create_context_aggregator(context)
|
|
22
|
+
|
|
23
|
+
# Context frame type
|
|
24
|
+
frame: OpenAILLMContextFrame
|
|
25
|
+
|
|
26
|
+
# Context type
|
|
27
|
+
context: AWSNovaSonicLLMContext
|
|
28
|
+
# or
|
|
29
|
+
context: OpenAILLMContext
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
AFTER:
|
|
33
|
+
```
|
|
34
|
+
# Setup
|
|
35
|
+
context = LLMContext(messages, tools)
|
|
36
|
+
context_aggregator = LLMContextAggregatorPair(context)
|
|
37
|
+
|
|
38
|
+
# Context frame type
|
|
39
|
+
frame: LLMContextFrame
|
|
40
|
+
|
|
41
|
+
# Context type
|
|
42
|
+
context: LLMContext
|
|
43
|
+
```
|
|
11
44
|
"""
|
|
12
45
|
|
|
46
|
+
import warnings
|
|
47
|
+
|
|
48
|
+
with warnings.catch_warnings():
|
|
49
|
+
warnings.simplefilter("always")
|
|
50
|
+
warnings.warn(
|
|
51
|
+
"Types in pipecat.services.aws.nova_sonic.context (or "
|
|
52
|
+
"pipecat.services.aws_nova_sonic.context) are deprecated. \n"
|
|
53
|
+
"AWS Nova Sonic no longer uses types from this module under the hood. \n"
|
|
54
|
+
"It now uses `LLMContext` and `LLMContextAggregatorPair`. \n"
|
|
55
|
+
"Using the new patterns should allow you to not need types from this module.\n\n"
|
|
56
|
+
"BEFORE:\n"
|
|
57
|
+
"```\n"
|
|
58
|
+
"# Setup\n"
|
|
59
|
+
"context = OpenAILLMContext(messages, tools)\n"
|
|
60
|
+
"context_aggregator = llm.create_context_aggregator(context)\n\n"
|
|
61
|
+
"# Context frame type\n"
|
|
62
|
+
"frame: OpenAILLMContextFrame\n\n"
|
|
63
|
+
"# Context type\n"
|
|
64
|
+
"context: AWSNovaSonicLLMContext\n"
|
|
65
|
+
"# or\n"
|
|
66
|
+
"context: OpenAILLMContext\n\n"
|
|
67
|
+
"```\n\n"
|
|
68
|
+
"AFTER:\n"
|
|
69
|
+
"```\n"
|
|
70
|
+
"# Setup\n"
|
|
71
|
+
"context = LLMContext(messages, tools)\n"
|
|
72
|
+
"context_aggregator = LLMContextAggregatorPair(context)\n\n"
|
|
73
|
+
"# Context frame type\n"
|
|
74
|
+
"frame: LLMContextFrame\n\n"
|
|
75
|
+
"# Context type\n"
|
|
76
|
+
"context: LLMContext\n\n"
|
|
77
|
+
"```",
|
|
78
|
+
DeprecationWarning,
|
|
79
|
+
stacklevel=2,
|
|
80
|
+
)
|
|
81
|
+
|
|
13
82
|
import copy
|
|
14
83
|
from dataclasses import dataclass, field
|
|
15
84
|
from enum import Enum
|