diff --git a/.github/workflows/pre-pr-drift-check.yml b/.github/workflows/pre-pr-drift-check.yml index c0ca175..d3b2ad0 100644 --- a/.github/workflows/pre-pr-drift-check.yml +++ b/.github/workflows/pre-pr-drift-check.yml @@ -4,13 +4,13 @@ on: pull_request: branches: [main] paths: - - 'api/**' - - 'ui/**' - - 'pipecat/**' - - 'scripts/dump_docs_openapi.py' - - 'scripts/format.sh' - - 'docs/api-reference/openapi.json' - - '.github/workflows/pre-pr-drift-check.yml' + - "api/**" + - "ui/**" + - "pipecat/**" + - "scripts/dump_docs_openapi.py" + - "scripts/format.sh" + - "docs/api-reference/openapi.json" + - ".github/workflows/pre-pr-drift-check.yml" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -30,7 +30,7 @@ jobs: - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" cache: pip cache-dependency-path: | api/requirements.txt @@ -40,15 +40,12 @@ jobs: - name: Set up Node 22 uses: actions/setup-node@v4 with: - node-version: '22' + node-version: "22" cache: npm cache-dependency-path: ui/package-lock.json - - name: Install api dependencies - run: | - pip install -r api/requirements.txt - pip install -r api/requirements.dev.txt - pip install './pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]' + - name: Install api and pipecat dependencies + run: ./scripts/setup_requirements.sh --dev - name: Install ui dependencies working-directory: ui diff --git a/api/logging_config.py b/api/logging_config.py index bd4649f..3e4f443 100644 --- a/api/logging_config.py +++ b/api/logging_config.py @@ -3,6 +3,7 @@ import os import sys import loguru +from pipecat.utils.run_context import run_id_var from api.constants import ( ENVIRONMENT, @@ -15,7 +16,6 @@ from api.constants import ( ) from api.enums import Environment from api.utils.worker import get_worker_id, is_worker_process -from pipecat.utils.run_context import run_id_var # Track if logging has been initialized _logging_initialized = False diff --git a/api/requirements.dev.txt b/api/requirements.dev.txt index cebdad0..b5bd106 100644 --- a/api/requirements.dev.txt +++ b/api/requirements.dev.txt @@ -1,11 +1,5 @@ -mypy==1.15.0 -ruff==0.11.3 -pytest==8.3.5 -pytest-asyncio==0.26.0 -pre-commit==4.2.0 -watchfiles==1.1.0 -python-dotenv==1.2.1 +mypy==2.0.0 +watchfiles==1.1.1 datamodel-code-generator==0.56.1 twine==6.2.0 -build==1.2.2 -e ./sdk/python diff --git a/api/routes/agent_stream.py b/api/routes/agent_stream.py index 2debe7a..b593a31 100644 --- a/api/routes/agent_stream.py +++ b/api/routes/agent_stream.py @@ -17,13 +17,13 @@ from typing import Optional from fastapi import APIRouter, WebSocket from loguru import logger +from pipecat.utils.run_context import set_current_org_id, set_current_run_id from starlette.websockets import WebSocketDisconnect from api.db import db_client from api.enums import CallType, WorkflowRunState from api.services.quota_service import check_dograh_quota_by_user_id from api.services.telephony import registry as telephony_registry -from pipecat.utils.run_context import set_current_org_id, set_current_run_id router = APIRouter(prefix="/agent-stream") diff --git a/api/routes/telephony.py b/api/routes/telephony.py index b21ce05..3b14406 100644 --- a/api/routes/telephony.py +++ b/api/routes/telephony.py @@ -15,6 +15,7 @@ from fastapi import ( WebSocket, ) from loguru import logger +from pipecat.utils.run_context import set_current_run_id from pydantic import BaseModel from starlette.websockets import WebSocketDisconnect @@ -43,7 +44,6 @@ from api.utils.telephony_helper import ( numbers_match, parse_webhook_request, ) -from pipecat.utils.run_context import set_current_run_id router = APIRouter(prefix="/telephony") diff --git a/api/routes/webrtc_signaling.py b/api/routes/webrtc_signaling.py index 04eee4b..4246b0b 100644 --- a/api/routes/webrtc_signaling.py +++ b/api/routes/webrtc_signaling.py @@ -24,6 +24,8 @@ from aiortc import RTCIceServer from aiortc.sdp import candidate_from_sdp from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect from loguru import logger +from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection +from pipecat.utils.run_context import set_current_org_id, set_current_run_id from starlette.websockets import WebSocketState from api.constants import ENVIRONMENT @@ -43,8 +45,6 @@ from api.services.pipecat.ws_sender_registry import ( unregister_ws_sender, ) from api.services.quota_service import check_dograh_quota -from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection -from pipecat.utils.run_context import set_current_org_id, set_current_run_id router = APIRouter(prefix="/ws") diff --git a/api/services/looptalk/audio_streamer.py b/api/services/looptalk/audio_streamer.py index 8221c4d..0acdb22 100644 --- a/api/services/looptalk/audio_streamer.py +++ b/api/services/looptalk/audio_streamer.py @@ -9,7 +9,6 @@ import asyncio from typing import Dict, Set from loguru import logger - from pipecat.audio.utils import mix_audio from pipecat.frames.frames import ( Frame, diff --git a/api/services/looptalk/core/pipeline_builder.py b/api/services/looptalk/core/pipeline_builder.py index ee11613..77e634f 100644 --- a/api/services/looptalk/core/pipeline_builder.py +++ b/api/services/looptalk/core/pipeline_builder.py @@ -3,6 +3,10 @@ from typing import Any, Dict from loguru import logger +from pipecat.pipeline.pipeline import Pipeline +from pipecat.processors.aggregators.llm_response_universal import ( + LLMContextAggregatorPair, +) from api.db.db_client import DBClient from api.services.looptalk.audio_streamer import get_or_create_audio_streamer @@ -23,10 +27,6 @@ from api.services.pipecat.service_factory import ( from api.services.workflow.dto import ReactFlowDTO from api.services.workflow.pipecat_engine import PipecatEngine from api.services.workflow.workflow import WorkflowGraph -from pipecat.pipeline.pipeline import Pipeline -from pipecat.processors.aggregators.llm_response_universal import ( - LLMContextAggregatorPair, -) class LoopTalkPipelineBuilder: diff --git a/api/services/looptalk/internal_serializer.py b/api/services/looptalk/internal_serializer.py index 75d89ea..6a94a40 100644 --- a/api/services/looptalk/internal_serializer.py +++ b/api/services/looptalk/internal_serializer.py @@ -7,7 +7,6 @@ """Internal frame serializer for agent-to-agent communication.""" from loguru import logger - from pipecat.frames.frames import ( Frame, InputAudioRawFrame, diff --git a/api/services/looptalk/internal_transport.py b/api/services/looptalk/internal_transport.py index 756b499..00bfc93 100644 --- a/api/services/looptalk/internal_transport.py +++ b/api/services/looptalk/internal_transport.py @@ -11,8 +11,6 @@ import time from typing import Dict, Optional, Tuple from loguru import logger - -from api.services.looptalk.internal_serializer import InternalFrameSerializer from pipecat.frames.frames import ( CancelFrame, EndFrame, @@ -29,6 +27,8 @@ from pipecat.transports.base_input import BaseInputTransport from pipecat.transports.base_output import BaseOutputTransport from pipecat.transports.base_transport import BaseTransport, TransportParams +from api.services.looptalk.internal_serializer import InternalFrameSerializer + class InternalInputTransport(BaseInputTransport): """Input side of internal transport for agent-to-agent communication.""" diff --git a/api/services/looptalk/orchestrator.py b/api/services/looptalk/orchestrator.py index d4c969a..d51e9da 100644 --- a/api/services/looptalk/orchestrator.py +++ b/api/services/looptalk/orchestrator.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import Any, Dict, Optional from loguru import logger +from pipecat.pipeline.task import PipelineTask +from pipecat.utils.run_context import set_current_run_id from api.db.db_client import DBClient from api.services.looptalk.internal_transport import ( @@ -13,8 +15,6 @@ from api.services.looptalk.internal_transport import ( InternalTransportManager, ) from api.services.pipecat.transport_setup import create_internal_transport -from pipecat.pipeline.task import PipelineTask -from pipecat.utils.run_context import set_current_run_id from .core.pipeline_builder import LoopTalkPipelineBuilder from .core.recording_manager import RecordingManager diff --git a/api/services/smart_turn/app.py b/api/services/smart_turn/app.py index 66ccf5a..6bbd0ab 100644 --- a/api/services/smart_turn/app.py +++ b/api/services/smart_turn/app.py @@ -21,9 +21,8 @@ from fastapi import ( status, ) from fastapi.websockets import WebSocketState -from scipy.io import wavfile - from pipecat.audio.turn.smart_turn.local_smart_turn_v2 import LocalSmartTurnAnalyzerV2 +from scipy.io import wavfile LOG_LEVEL = ( logging.DEBUG diff --git a/api/services/smart_turn/websocket_smart_turn.py b/api/services/smart_turn/websocket_smart_turn.py index 4220d3c..82a7e6f 100644 --- a/api/services/smart_turn/websocket_smart_turn.py +++ b/api/services/smart_turn/websocket_smart_turn.py @@ -20,7 +20,6 @@ from typing import Any, Dict, Optional import numpy as np import websockets from loguru import logger - from pipecat.audio.turn.smart_turn.base_smart_turn import ( BaseSmartTurn, SmartTurnTimeoutException, diff --git a/api/services/telephony/providers/ari/strategies.py b/api/services/telephony/providers/ari/strategies.py index 4e02c8e..288110f 100644 --- a/api/services/telephony/providers/ari/strategies.py +++ b/api/services/telephony/providers/ari/strategies.py @@ -6,7 +6,6 @@ This module contains the business logic for Asterisk ARI call operations. from typing import Any, Dict from loguru import logger - from pipecat.serializers.call_strategies import HangupStrategy, TransferStrategy diff --git a/api/services/telephony/providers/ari/transport.py b/api/services/telephony/providers/ari/transport.py index 3f9ceb2..58efea5 100644 --- a/api/services/telephony/providers/ari/transport.py +++ b/api/services/telephony/providers/ari/transport.py @@ -1,15 +1,15 @@ """ARI (Asterisk) transport factory.""" from fastapi import WebSocket - -from api.services.pipecat.audio_config import AudioConfig -from api.services.pipecat.audio_mixer import build_audio_out_mixer -from api.services.telephony.factory import load_credentials_for_transport from pipecat.transports.websocket.fastapi import ( FastAPIWebsocketParams, FastAPIWebsocketTransport, ) +from api.services.pipecat.audio_config import AudioConfig +from api.services.pipecat.audio_mixer import build_audio_out_mixer +from api.services.telephony.factory import load_credentials_for_transport + from .serializers import AsteriskFrameSerializer from .strategies import ARIBridgeSwapStrategy, ARIHangupStrategy diff --git a/api/services/telephony/providers/cloudonix/routes.py b/api/services/telephony/providers/cloudonix/routes.py index 6f39831..cd4758a 100644 --- a/api/services/telephony/providers/cloudonix/routes.py +++ b/api/services/telephony/providers/cloudonix/routes.py @@ -8,6 +8,7 @@ import json from fastapi import APIRouter, Request from loguru import logger +from pipecat.utils.run_context import set_current_run_id from api.db import db_client from api.services.telephony.factory import get_telephony_provider_for_run @@ -15,7 +16,6 @@ from api.services.telephony.status_processor import ( StatusCallbackRequest, _process_status_update, ) -from pipecat.utils.run_context import set_current_run_id router = APIRouter() diff --git a/api/services/telephony/providers/cloudonix/strategies.py b/api/services/telephony/providers/cloudonix/strategies.py index 1fa4fad..b64cf6d 100644 --- a/api/services/telephony/providers/cloudonix/strategies.py +++ b/api/services/telephony/providers/cloudonix/strategies.py @@ -3,9 +3,9 @@ from typing import Any, Dict from loguru import logger +from pipecat.serializers.call_strategies import HangupStrategy from api.services.telephony.providers.cloudonix.provider import CLOUDONIX_API_BASE_URL -from pipecat.serializers.call_strategies import HangupStrategy class CloudonixHangupStrategy(HangupStrategy): diff --git a/api/services/telephony/providers/cloudonix/transport.py b/api/services/telephony/providers/cloudonix/transport.py index 33d58ef..cd91518 100644 --- a/api/services/telephony/providers/cloudonix/transport.py +++ b/api/services/telephony/providers/cloudonix/transport.py @@ -1,15 +1,15 @@ """Cloudonix transport factory.""" from fastapi import WebSocket - -from api.services.pipecat.audio_config import AudioConfig -from api.services.pipecat.audio_mixer import build_audio_out_mixer -from api.services.telephony.factory import load_credentials_for_transport from pipecat.transports.websocket.fastapi import ( FastAPIWebsocketParams, FastAPIWebsocketTransport, ) +from api.services.pipecat.audio_config import AudioConfig +from api.services.pipecat.audio_mixer import build_audio_out_mixer +from api.services.telephony.factory import load_credentials_for_transport + from .serializers import CloudonixFrameSerializer from .strategies import CloudonixHangupStrategy diff --git a/api/services/telephony/providers/plivo/routes.py b/api/services/telephony/providers/plivo/routes.py index 6fad8c3..be1ecd7 100644 --- a/api/services/telephony/providers/plivo/routes.py +++ b/api/services/telephony/providers/plivo/routes.py @@ -9,6 +9,7 @@ from typing import Optional from fastapi import APIRouter, Header, Request from loguru import logger +from pipecat.utils.run_context import set_current_run_id from starlette.responses import HTMLResponse from api.db import db_client @@ -18,7 +19,6 @@ from api.services.telephony.status_processor import ( _process_status_update, ) from api.utils.common import get_backend_endpoints -from pipecat.utils.run_context import set_current_run_id router = APIRouter() diff --git a/api/services/telephony/providers/plivo/transport.py b/api/services/telephony/providers/plivo/transport.py index ce60f42..039c562 100644 --- a/api/services/telephony/providers/plivo/transport.py +++ b/api/services/telephony/providers/plivo/transport.py @@ -1,15 +1,15 @@ """Plivo transport factory.""" from fastapi import WebSocket - -from api.services.pipecat.audio_config import AudioConfig -from api.services.pipecat.audio_mixer import build_audio_out_mixer -from api.services.telephony.factory import load_credentials_for_transport from pipecat.transports.websocket.fastapi import ( FastAPIWebsocketParams, FastAPIWebsocketTransport, ) +from api.services.pipecat.audio_config import AudioConfig +from api.services.pipecat.audio_mixer import build_audio_out_mixer +from api.services.telephony.factory import load_credentials_for_transport + from .serializers import PlivoFrameSerializer diff --git a/api/services/telephony/providers/telnyx/provider.py b/api/services/telephony/providers/telnyx/provider.py index 72e87d7..d127dfa 100644 --- a/api/services/telephony/providers/telnyx/provider.py +++ b/api/services/telephony/providers/telnyx/provider.py @@ -9,7 +9,7 @@ import random from typing import TYPE_CHECKING, Any, Dict, List, Optional import aiohttp -from fastapi import HTTPException +from fastapi import HTTPException, WebSocketDisconnect from loguru import logger from api.enums import WorkflowRunMode @@ -22,8 +22,6 @@ from api.services.telephony.base import ( from api.utils.common import get_backend_endpoints from api.utils.telephony_address import normalize_telephony_address -from fastapi import WebSocketDisconnect - if TYPE_CHECKING: from fastapi import WebSocket diff --git a/api/services/telephony/providers/telnyx/routes.py b/api/services/telephony/providers/telnyx/routes.py index 23df07e..0947b14 100644 --- a/api/services/telephony/providers/telnyx/routes.py +++ b/api/services/telephony/providers/telnyx/routes.py @@ -8,6 +8,7 @@ import json from fastapi import APIRouter, Request from loguru import logger +from pipecat.utils.run_context import set_current_run_id from api.db import db_client from api.services.telephony.factory import get_telephony_provider_for_run @@ -16,7 +17,6 @@ from api.services.telephony.status_processor import ( StatusCallbackRequest, _process_status_update, ) -from pipecat.utils.run_context import set_current_run_id router = APIRouter() diff --git a/api/services/telephony/providers/telnyx/transport.py b/api/services/telephony/providers/telnyx/transport.py index bdb3f47..c2b96f4 100644 --- a/api/services/telephony/providers/telnyx/transport.py +++ b/api/services/telephony/providers/telnyx/transport.py @@ -1,15 +1,15 @@ """Telnyx transport factory.""" from fastapi import WebSocket - -from api.services.pipecat.audio_config import AudioConfig -from api.services.pipecat.audio_mixer import build_audio_out_mixer -from api.services.telephony.factory import load_credentials_for_transport from pipecat.transports.websocket.fastapi import ( FastAPIWebsocketParams, FastAPIWebsocketTransport, ) +from api.services.pipecat.audio_config import AudioConfig +from api.services.pipecat.audio_mixer import build_audio_out_mixer +from api.services.telephony.factory import load_credentials_for_transport + from .serializers import TelnyxFrameSerializer diff --git a/api/services/telephony/providers/twilio/routes.py b/api/services/telephony/providers/twilio/routes.py index 11fca1b..e8ac939 100644 --- a/api/services/telephony/providers/twilio/routes.py +++ b/api/services/telephony/providers/twilio/routes.py @@ -9,6 +9,7 @@ from typing import Optional from fastapi import APIRouter, Header, Request from loguru import logger +from pipecat.utils.run_context import set_current_run_id from starlette.responses import HTMLResponse from api.db import db_client @@ -18,7 +19,6 @@ from api.services.telephony.status_processor import ( _process_status_update, ) from api.utils.common import get_backend_endpoints -from pipecat.utils.run_context import set_current_run_id router = APIRouter() diff --git a/api/services/telephony/providers/twilio/strategies.py b/api/services/telephony/providers/twilio/strategies.py index 003eb33..e80e1a6 100644 --- a/api/services/telephony/providers/twilio/strategies.py +++ b/api/services/telephony/providers/twilio/strategies.py @@ -8,7 +8,6 @@ from typing import Any, Dict import aiohttp from loguru import logger - from pipecat.serializers.call_strategies import HangupStrategy, TransferStrategy diff --git a/api/services/telephony/providers/twilio/transport.py b/api/services/telephony/providers/twilio/transport.py index 823e05c..d3a4937 100644 --- a/api/services/telephony/providers/twilio/transport.py +++ b/api/services/telephony/providers/twilio/transport.py @@ -1,15 +1,15 @@ """Twilio transport factory.""" from fastapi import WebSocket - -from api.services.pipecat.audio_config import AudioConfig -from api.services.pipecat.audio_mixer import build_audio_out_mixer -from api.services.telephony.factory import load_credentials_for_transport from pipecat.transports.websocket.fastapi import ( FastAPIWebsocketParams, FastAPIWebsocketTransport, ) +from api.services.pipecat.audio_config import AudioConfig +from api.services.pipecat.audio_mixer import build_audio_out_mixer +from api.services.telephony.factory import load_credentials_for_transport + from .serializers import TwilioFrameSerializer from .strategies import TwilioConferenceStrategy, TwilioHangupStrategy diff --git a/api/services/telephony/providers/vobiz/routes.py b/api/services/telephony/providers/vobiz/routes.py index d39946c..4fffe5b 100644 --- a/api/services/telephony/providers/vobiz/routes.py +++ b/api/services/telephony/providers/vobiz/routes.py @@ -10,6 +10,7 @@ from typing import Optional from fastapi import APIRouter, Header, Request from loguru import logger +from pipecat.utils.run_context import set_current_run_id from starlette.responses import HTMLResponse from api.db import db_client @@ -24,7 +25,6 @@ from api.utils.common import get_backend_endpoints from api.utils.telephony_helper import ( parse_webhook_request, ) -from pipecat.utils.run_context import set_current_run_id router = APIRouter() diff --git a/api/services/telephony/providers/vobiz/transport.py b/api/services/telephony/providers/vobiz/transport.py index 1a4b781..46ac392 100644 --- a/api/services/telephony/providers/vobiz/transport.py +++ b/api/services/telephony/providers/vobiz/transport.py @@ -7,15 +7,15 @@ Vobiz uses Plivo-compatible WebSocket protocol: from fastapi import WebSocket from loguru import logger - -from api.services.pipecat.audio_config import AudioConfig -from api.services.pipecat.audio_mixer import build_audio_out_mixer -from api.services.telephony.factory import load_credentials_for_transport from pipecat.transports.websocket.fastapi import ( FastAPIWebsocketParams, FastAPIWebsocketTransport, ) +from api.services.pipecat.audio_config import AudioConfig +from api.services.pipecat.audio_mixer import build_audio_out_mixer +from api.services.telephony.factory import load_credentials_for_transport + from .serializers import VobizFrameSerializer diff --git a/api/services/telephony/providers/vonage/routes.py b/api/services/telephony/providers/vonage/routes.py index dff1bba..a4cca35 100644 --- a/api/services/telephony/providers/vonage/routes.py +++ b/api/services/telephony/providers/vonage/routes.py @@ -9,6 +9,7 @@ from typing import Optional from fastapi import APIRouter, Request from loguru import logger +from pipecat.utils.run_context import set_current_run_id from api.db import db_client from api.services.telephony.factory import get_telephony_provider_for_run @@ -16,7 +17,6 @@ from api.services.telephony.status_processor import ( StatusCallbackRequest, _process_status_update, ) -from pipecat.utils.run_context import set_current_run_id router = APIRouter() diff --git a/api/services/telephony/providers/vonage/transport.py b/api/services/telephony/providers/vonage/transport.py index 7183702..0fae27f 100644 --- a/api/services/telephony/providers/vonage/transport.py +++ b/api/services/telephony/providers/vonage/transport.py @@ -1,13 +1,14 @@ """Vonage transport factory.""" -from api.services.pipecat.audio_config import AudioConfig -from api.services.pipecat.audio_mixer import build_audio_out_mixer -from api.services.telephony.factory import load_credentials_for_transport from pipecat.transports.websocket.fastapi import ( FastAPIWebsocketParams, FastAPIWebsocketTransport, ) +from api.services.pipecat.audio_config import AudioConfig +from api.services.pipecat.audio_mixer import build_audio_out_mixer +from api.services.telephony.factory import load_credentials_for_transport + from .serializers import VonageFrameSerializer diff --git a/api/services/workflow/pipecat_engine.py b/api/services/workflow/pipecat_engine.py index fc84595..b4f00cb 100644 --- a/api/services/workflow/pipecat_engine.py +++ b/api/services/workflow/pipecat_engine.py @@ -1,9 +1,5 @@ from typing import TYPE_CHECKING, Awaitable, Callable, Optional, Union -from api.db import db_client -from api.services.pipecat.audio_playback import play_audio -from api.services.workflow.disposition_mapper import apply_disposition_mapping -from api.services.workflow.workflow import Node, WorkflowGraph from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.frames.frames import ( BotStartedSpeakingFrame, @@ -19,6 +15,11 @@ from pipecat.services.llm_service import FunctionCallParams from pipecat.services.settings import LLMSettings from pipecat.utils.enums import EndTaskReason +from api.db import db_client +from api.services.pipecat.audio_playback import play_audio +from api.services.workflow.disposition_mapper import apply_disposition_mapping +from api.services.workflow.workflow import Node, WorkflowGraph + if TYPE_CHECKING: from pipecat.frames.frames import Frame from pipecat.services.anthropic.llm import AnthropicLLMService diff --git a/api/services/workflow/pipecat_engine_callbacks.py b/api/services/workflow/pipecat_engine_callbacks.py index 83990bf..87ff06e 100644 --- a/api/services/workflow/pipecat_engine_callbacks.py +++ b/api/services/workflow/pipecat_engine_callbacks.py @@ -14,7 +14,6 @@ import re from typing import TYPE_CHECKING from loguru import logger - from pipecat.frames.frames import ( LLMMessagesAppendFrame, ) diff --git a/api/services/workflow/pipecat_engine_context_summarizer.py b/api/services/workflow/pipecat_engine_context_summarizer.py index 1ea9f47..abfa7b2 100644 --- a/api/services/workflow/pipecat_engine_context_summarizer.py +++ b/api/services/workflow/pipecat_engine_context_summarizer.py @@ -6,8 +6,6 @@ from typing import TYPE_CHECKING, Optional from loguru import logger from opentelemetry import trace - -from api.services.pipecat.tracing_config import ensure_tracing from pipecat.frames.frames import LLMContextSummaryRequestFrame from pipecat.utils.context.llm_context_summarization import ( LLMContextSummarizationUtil, @@ -15,6 +13,8 @@ from pipecat.utils.context.llm_context_summarization import ( ) from pipecat.utils.tracing.service_attributes import add_llm_span_attributes +from api.services.pipecat.tracing_config import ensure_tracing + if TYPE_CHECKING: from api.services.workflow.pipecat_engine import PipecatEngine diff --git a/api/services/workflow/pipecat_engine_custom_tools.py b/api/services/workflow/pipecat_engine_custom_tools.py index 91922b8..f4b5a2b 100644 --- a/api/services/workflow/pipecat_engine_custom_tools.py +++ b/api/services/workflow/pipecat_engine_custom_tools.py @@ -13,6 +13,13 @@ import uuid from typing import TYPE_CHECKING, Any, Dict, List, Optional from loguru import logger +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.frames.frames import ( + FunctionCallResultProperties, + TTSSpeakFrame, +) +from pipecat.services.llm_service import FunctionCallParams +from pipecat.utils.enums import EndTaskReason from api.db import db_client from api.enums import ToolCategory, WorkflowRunMode @@ -25,13 +32,6 @@ from api.services.workflow.tools.custom_tool import ( execute_http_tool, tool_to_function_schema, ) -from pipecat.adapters.schemas.function_schema import FunctionSchema -from pipecat.frames.frames import ( - FunctionCallResultProperties, - TTSSpeakFrame, -) -from pipecat.services.llm_service import FunctionCallParams -from pipecat.utils.enums import EndTaskReason if TYPE_CHECKING: from api.services.workflow.pipecat_engine import PipecatEngine diff --git a/api/services/workflow/pipecat_engine_variable_extractor.py b/api/services/workflow/pipecat_engine_variable_extractor.py index 53996cd..2853403 100644 --- a/api/services/workflow/pipecat_engine_variable_extractor.py +++ b/api/services/workflow/pipecat_engine_variable_extractor.py @@ -5,12 +5,12 @@ from typing import TYPE_CHECKING, Any, List from loguru import logger from opentelemetry import trace +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.utils.tracing.service_attributes import add_llm_span_attributes from api.services.gen_ai.json_parser import parse_llm_json from api.services.pipecat.tracing_config import ensure_tracing from api.services.workflow.dto import ExtractionVariableDTO -from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.utils.tracing.service_attributes import add_llm_span_attributes if TYPE_CHECKING: from api.services.workflow.pipecat_engine import PipecatEngine diff --git a/api/services/workflow/qa/analysis.py b/api/services/workflow/qa/analysis.py index b0a171e..0afb2e1 100644 --- a/api/services/workflow/qa/analysis.py +++ b/api/services/workflow/qa/analysis.py @@ -4,6 +4,7 @@ import json from typing import Any from loguru import logger +from pipecat.processors.aggregators.llm_context import LLMContext from api.db.models import WorkflowRunModel from api.services.gen_ai.json_parser import parse_llm_json @@ -26,7 +27,6 @@ from api.services.workflow.qa.tracing import ( setup_langfuse_parent_context, ) from api.utils.template_renderer import render_template -from pipecat.processors.aggregators.llm_context import LLMContext async def _run_llm_inference( diff --git a/api/services/workflow/qa/node_summary.py b/api/services/workflow/qa/node_summary.py index 5896f4c..aaeb7d3 100644 --- a/api/services/workflow/qa/node_summary.py +++ b/api/services/workflow/qa/node_summary.py @@ -3,6 +3,7 @@ from typing import Any from loguru import logger +from pipecat.processors.aggregators.llm_context import LLMContext from api.db import db_client from api.db.models import WorkflowRunModel @@ -10,7 +11,6 @@ from api.services.pipecat.service_factory import create_llm_service_from_provide from api.services.workflow.dto import NodeType, QANodeData from api.services.workflow.qa.llm_config import resolve_llm_config from api.services.workflow.qa.tracing import create_node_summary_trace -from pipecat.processors.aggregators.llm_context import LLMContext NODE_SUMMARY_SYSTEM_PROMPT = ( "You are analyzing a voice AI agent script. This is only a part of a larger script. " diff --git a/api/services/workflow/qa/tracing.py b/api/services/workflow/qa/tracing.py index d3a5ff1..58a0843 100644 --- a/api/services/workflow/qa/tracing.py +++ b/api/services/workflow/qa/tracing.py @@ -78,7 +78,6 @@ def add_qa_span_to_trace( return try: from opentelemetry import trace as otel_trace - from pipecat.utils.tracing.service_attributes import add_llm_span_attributes tracer = otel_trace.get_tracer("pipecat") @@ -122,9 +121,9 @@ def create_node_summary_trace( try: from opentelemetry import trace as otel_trace from opentelemetry.context import Context + from pipecat.utils.tracing.service_attributes import add_llm_span_attributes from api.services.pipecat.tracing_config import ensure_tracing - from pipecat.utils.tracing.service_attributes import add_llm_span_attributes if not ensure_tracing(): return None diff --git a/api/tasks/run_integrations.py b/api/tasks/run_integrations.py index da87413..d73a39b 100644 --- a/api/tasks/run_integrations.py +++ b/api/tasks/run_integrations.py @@ -5,6 +5,8 @@ from typing import Any, Dict, Optional import httpx from loguru import logger +from pipecat.utils.enums import EndTaskReason +from pipecat.utils.run_context import set_current_org_id, set_current_run_id from pydantic import ValidationError from api.constants import BACKEND_API_ENDPOINT @@ -21,8 +23,6 @@ from api.services.workflow.dto import ( from api.services.workflow.qa import run_per_node_qa_analysis from api.utils.credential_auth import build_auth_header from api.utils.template_renderer import render_template -from pipecat.utils.enums import EndTaskReason -from pipecat.utils.run_context import set_current_org_id, set_current_run_id def _should_skip_qa( diff --git a/api/tasks/s3_upload.py b/api/tasks/s3_upload.py index c18a9b3..b2086c0 100644 --- a/api/tasks/s3_upload.py +++ b/api/tasks/s3_upload.py @@ -2,12 +2,12 @@ import os from typing import Optional from loguru import logger +from pipecat.utils.run_context import set_current_run_id from api.db import db_client from api.services.pricing.workflow_run_cost import calculate_workflow_run_cost from api.services.storage import get_current_storage_backend, storage_fs from api.tasks.run_integrations import run_integrations_post_workflow_run -from pipecat.utils.run_context import set_current_run_id async def upload_voicemail_audio_to_s3( diff --git a/api/tests/integrations/_run_pipeline_helpers.py b/api/tests/integrations/_run_pipeline_helpers.py index cacbf50..0591c09 100644 --- a/api/tests/integrations/_run_pipeline_helpers.py +++ b/api/tests/integrations/_run_pipeline_helpers.py @@ -29,11 +29,12 @@ from contextlib import ExitStack, contextmanager from typing import Any from unittest.mock import AsyncMock, patch -from api.db.models import OrganizationModel, UserModel -from api.enums import WorkflowRunMode from pipecat.frames.frames import Frame from pipecat.observers.base_observer import BaseObserver from pipecat.processors.frame_processor import FrameDirection, FrameProcessor + +from api.db.models import OrganizationModel, UserModel +from api.enums import WorkflowRunMode from pipecat.tests import MockLLMService, MockTTSService USER_CONFIGURATION: dict[str, Any] = { diff --git a/api/tests/integrations/test_run_pipeline.py b/api/tests/integrations/test_run_pipeline.py index 8a8da88..9a87aa1 100644 --- a/api/tests/integrations/test_run_pipeline.py +++ b/api/tests/integrations/test_run_pipeline.py @@ -17,6 +17,8 @@ completion flag, and ``gathered_context`` entries. import asyncio import pytest +from pipecat.tests.mock_transport import MockTransport +from pipecat.transports.base_transport import TransportParams from api.enums import WorkflowRunMode, WorkflowRunState from api.services.pipecat.audio_config import create_audio_config @@ -25,8 +27,6 @@ from api.tests.integrations._run_pipeline_helpers import ( create_workflow_run_rows, patch_run_pipeline_externals, ) -from pipecat.tests.mock_transport import MockTransport -from pipecat.transports.base_transport import TransportParams WORKFLOW_DEFINITION = { "nodes": [ diff --git a/api/tests/integrations/test_run_pipeline_text_greeting.py b/api/tests/integrations/test_run_pipeline_text_greeting.py index 5003515..0da7bf8 100644 --- a/api/tests/integrations/test_run_pipeline_text_greeting.py +++ b/api/tests/integrations/test_run_pipeline_text_greeting.py @@ -28,6 +28,10 @@ deterministic and the synthesised audio length is short. import asyncio import pytest +from pipecat.frames.frames import TranscriptionFrame +from pipecat.tests.mock_transport import MockTransport +from pipecat.transports.base_transport import TransportParams +from pipecat.utils.time import time_now_iso8601 from api.enums import WorkflowRunMode, WorkflowRunState from api.services.pipecat.audio_config import create_audio_config @@ -36,11 +40,7 @@ from api.tests.integrations._run_pipeline_helpers import ( create_workflow_run_rows, patch_run_pipeline_externals, ) -from pipecat.frames.frames import TranscriptionFrame from pipecat.tests import MockLLMService, MockTTSService -from pipecat.tests.mock_transport import MockTransport -from pipecat.transports.base_transport import TransportParams -from pipecat.utils.time import time_now_iso8601 GREETING_TEXT = ( "Thanks for calling Happy Feet, this is Sarah. How can I help you today?" diff --git a/api/tests/test_custom_tools.py b/api/tests/test_custom_tools.py index b534902..693afc7 100644 --- a/api/tests/test_custom_tools.py +++ b/api/tests/test_custom_tools.py @@ -12,12 +12,6 @@ from typing import Any, Dict from unittest.mock import AsyncMock, Mock, patch import pytest - -from api.services.workflow.pipecat_engine_custom_tools import get_function_schema -from api.services.workflow.tools.custom_tool import ( - execute_http_tool, - tool_to_function_schema, -) from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.frames.frames import ( FunctionCallInProgressFrame, @@ -31,6 +25,12 @@ from pipecat.frames.frames import ( from pipecat.pipeline.pipeline import Pipeline from pipecat.processors.aggregators.llm_context import LLMContext from pipecat.services.llm_service import FunctionCallParams + +from api.services.workflow.pipecat_engine_custom_tools import get_function_schema +from api.services.workflow.tools.custom_tool import ( + execute_http_tool, + tool_to_function_schema, +) from pipecat.tests import MockLLMService, run_test @@ -721,9 +721,10 @@ class TestCustomToolManagerUnit: async def test_get_tool_schemas_returns_correct_format(self): """Test that get_tool_schemas returns FunctionSchema objects.""" # Create a mock engine + from pipecat.adapters.schemas.function_schema import FunctionSchema + from api.services.workflow.pipecat_engine import PipecatEngine from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager - from pipecat.adapters.schemas.function_schema import FunctionSchema mock_engine = Mock() mock_engine._workflow_run_id = 1 diff --git a/api/tests/test_custom_tools_context_integration.py b/api/tests/test_custom_tools_context_integration.py index 8e6ddb5..db060fb 100644 --- a/api/tests/test_custom_tools_context_integration.py +++ b/api/tests/test_custom_tools_context_integration.py @@ -9,15 +9,15 @@ This module tests the full flow of: from unittest.mock import AsyncMock, patch import pytest +from pipecat.adapters.schemas.function_schema import FunctionSchema +from pipecat.adapters.schemas.tools_schema import ToolsSchema +from pipecat.processors.aggregators.llm_context import LLMContext from api.services.workflow.pipecat_engine_custom_tools import ( CustomToolManager, get_function_schema, ) from api.tests.conftest import MockToolModel -from pipecat.adapters.schemas.function_schema import FunctionSchema -from pipecat.adapters.schemas.tools_schema import ToolsSchema -from pipecat.processors.aggregators.llm_context import LLMContext def _update_llm_context(context, system_message, functions): diff --git a/api/tests/test_pipecat_engine_context_update.py b/api/tests/test_pipecat_engine_context_update.py index 34d3138..e22a575 100644 --- a/api/tests/test_pipecat_engine_context_update.py +++ b/api/tests/test_pipecat_engine_context_update.py @@ -18,14 +18,6 @@ from typing import List from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.workflow import WorkflowGraph -from api.tests.conftest import ( - AGENT_SYSTEM_PROMPT, - END_CALL_SYSTEM_PROMPT, - START_CALL_SYSTEM_PROMPT, -) from pipecat.frames.frames import LLMContextFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner @@ -35,13 +27,21 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMAssistantAggregatorParams, LLMContextAggregatorPair, ) +from pipecat.tests.mock_transport import MockTransport +from pipecat.transports.base_transport import TransportParams + +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.workflow import WorkflowGraph +from api.tests.conftest import ( + AGENT_SYSTEM_PROMPT, + END_CALL_SYSTEM_PROMPT, + START_CALL_SYSTEM_PROMPT, +) from pipecat.tests import ( ContextCapturingMockLLM, MockLLMService, MockTTSService, ) -from pipecat.tests.mock_transport import MockTransport -from pipecat.transports.base_transport import TransportParams async def run_pipeline_and_capture_context( diff --git a/api/tests/test_pipecat_engine_end_call.py b/api/tests/test_pipecat_engine_end_call.py index e1ff101..a0f8ac1 100644 --- a/api/tests/test_pipecat_engine_end_call.py +++ b/api/tests/test_pipecat_engine_end_call.py @@ -23,6 +23,23 @@ from typing import Any, Dict, List from unittest.mock import AsyncMock, patch import pytest +from pipecat.frames.frames import Frame, LLMContextFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.runner import PipelineRunner +from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_context import LLMContext +from pipecat.processors.aggregators.llm_response_universal import ( + LLMAssistantAggregatorParams, + LLMContextAggregatorPair, + LLMUserAggregatorParams, +) +from pipecat.tests.mock_transport import MockTransport +from pipecat.transports.base_transport import TransportParams +from pipecat.turns.user_mute import ( + CallbackUserMuteStrategy, + MuteUntilFirstBotCompleteUserMuteStrategy, +) +from pipecat.utils.enums import EndTaskReason from api.enums import ToolCategory from api.services.workflow.dto import ( @@ -42,24 +59,7 @@ from api.services.workflow.pipecat_engine_variable_extractor import ( ) from api.services.workflow.workflow import WorkflowGraph from api.tests.conftest import END_CALL_SYSTEM_PROMPT, START_CALL_SYSTEM_PROMPT -from pipecat.frames.frames import Frame, LLMContextFrame -from pipecat.pipeline.pipeline import Pipeline -from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.task import PipelineParams, PipelineTask -from pipecat.processors.aggregators.llm_context import LLMContext -from pipecat.processors.aggregators.llm_response_universal import ( - LLMAssistantAggregatorParams, - LLMContextAggregatorPair, - LLMUserAggregatorParams, -) from pipecat.tests import MockLLMService, MockTTSService -from pipecat.tests.mock_transport import MockTransport -from pipecat.transports.base_transport import TransportParams -from pipecat.turns.user_mute import ( - CallbackUserMuteStrategy, - MuteUntilFirstBotCompleteUserMuteStrategy, -) -from pipecat.utils.enums import EndTaskReason class EndCallTestHelper: diff --git a/api/tests/test_pipecat_engine_node_switch_with_user_speech.py b/api/tests/test_pipecat_engine_node_switch_with_user_speech.py index 5e29b4f..a19843b 100644 --- a/api/tests/test_pipecat_engine_node_switch_with_user_speech.py +++ b/api/tests/test_pipecat_engine_node_switch_with_user_speech.py @@ -15,9 +15,6 @@ import asyncio from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.workflow import WorkflowGraph from pipecat.frames.frames import ( Frame, FunctionCallResultFrame, @@ -36,7 +33,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMUserAggregatorParams, ) from pipecat.processors.frame_processor import FrameDirection, FrameProcessor -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams from pipecat.turns.user_mute import ( @@ -52,6 +48,10 @@ from pipecat.turns.user_stop import ( from pipecat.turns.user_turn_strategies import UserTurnStrategies from pipecat.utils.time import time_now_iso8601 +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.workflow import WorkflowGraph +from pipecat.tests import MockLLMService, MockTTSService + class UserSpeechInjector(FrameProcessor): """Processor that injects user speaking frames on FunctionCallResultFrame. diff --git a/api/tests/test_pipecat_engine_tool_calls.py b/api/tests/test_pipecat_engine_tool_calls.py index 07d73a3..aef2df6 100644 --- a/api/tests/test_pipecat_engine_tool_calls.py +++ b/api/tests/test_pipecat_engine_tool_calls.py @@ -9,10 +9,6 @@ from typing import Any, Dict, List from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.workflow import WorkflowGraph -from api.tests.conftest import END_CALL_SYSTEM_PROMPT from pipecat.frames.frames import LLMContextFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner @@ -22,10 +18,14 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMAssistantAggregatorParams, LLMContextAggregatorPair, ) -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.workflow import WorkflowGraph +from api.tests.conftest import END_CALL_SYSTEM_PROMPT +from pipecat.tests import MockLLMService, MockTTSService + async def run_pipeline_with_tool_calls( workflow: WorkflowGraph, diff --git a/api/tests/test_pipecat_engine_transition_mute.py b/api/tests/test_pipecat_engine_transition_mute.py index 844612e..3cc5220 100644 --- a/api/tests/test_pipecat_engine_transition_mute.py +++ b/api/tests/test_pipecat_engine_transition_mute.py @@ -13,12 +13,6 @@ import asyncio from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.pipecat_engine_variable_extractor import ( - VariableExtractionManager, -) -from api.services.workflow.workflow import WorkflowGraph from pipecat.frames.frames import LLMContextFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner @@ -29,7 +23,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, LLMUserAggregatorParams, ) -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams from pipecat.turns.user_mute import ( @@ -38,6 +31,13 @@ from pipecat.turns.user_mute import ( MuteUntilFirstBotCompleteUserMuteStrategy, ) +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.pipecat_engine_variable_extractor import ( + VariableExtractionManager, +) +from api.services.workflow.workflow import WorkflowGraph +from pipecat.tests import MockLLMService, MockTTSService + async def _build_engine_and_pipeline( workflow: WorkflowGraph, diff --git a/api/tests/test_pipecat_engine_variable_extraction.py b/api/tests/test_pipecat_engine_variable_extraction.py index 63ba808..29581d7 100644 --- a/api/tests/test_pipecat_engine_variable_extraction.py +++ b/api/tests/test_pipecat_engine_variable_extraction.py @@ -16,12 +16,6 @@ from typing import Any, Dict, List from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.pipecat_engine_variable_extractor import ( - VariableExtractionManager, -) -from api.services.workflow.workflow import WorkflowGraph from pipecat.frames.frames import LLMContextFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner @@ -31,10 +25,16 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMAssistantAggregatorParams, LLMContextAggregatorPair, ) -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.pipecat_engine_variable_extractor import ( + VariableExtractionManager, +) +from api.services.workflow.workflow import WorkflowGraph +from pipecat.tests import MockLLMService, MockTTSService + class TestVariableExtractionDuringTransitions: """Test that variable extraction is triggered for the correct node during transitions.""" diff --git a/api/tests/test_pipeline_cancellation.py b/api/tests/test_pipeline_cancellation.py index 932e4d7..053c59f 100644 --- a/api/tests/test_pipeline_cancellation.py +++ b/api/tests/test_pipeline_cancellation.py @@ -2,7 +2,6 @@ import asyncio import pytest from loguru import logger - from pipecat.frames.frames import ( EndTaskFrame, Frame, diff --git a/api/tests/test_recording_router_processor.py b/api/tests/test_recording_router_processor.py index 5ef2057..c86e6b4 100644 --- a/api/tests/test_recording_router_processor.py +++ b/api/tests/test_recording_router_processor.py @@ -12,6 +12,14 @@ and inspect what arrives downstream. from typing import Optional import pytest +from pipecat.frames.frames import ( + LLMFullResponseEndFrame, + LLMTextFrame, + TTSAudioRawFrame, + TTSStartedFrame, + TTSStoppedFrame, + TTSTextFrame, +) from api.services.pipecat.recording_audio_cache import RecordingAudio from api.services.pipecat.recording_router_processor import ( @@ -21,14 +29,6 @@ from api.services.workflow.pipecat_engine_context_composer import ( RECORDING_MARKER, TTS_MARKER, ) -from pipecat.frames.frames import ( - LLMFullResponseEndFrame, - LLMTextFrame, - TTSAudioRawFrame, - TTSStartedFrame, - TTSStoppedFrame, - TTSTextFrame, -) from pipecat.tests import run_test # --------------------------------------------------------------------------- diff --git a/api/tests/test_text_and_audio_playback.py b/api/tests/test_text_and_audio_playback.py index e6a31f7..a950c9b 100644 --- a/api/tests/test_text_and_audio_playback.py +++ b/api/tests/test_text_and_audio_playback.py @@ -11,21 +11,6 @@ from typing import Any, Dict, List from unittest.mock import AsyncMock, Mock, patch import pytest - -from api.services.pipecat.recording_audio_cache import RecordingAudio -from api.services.workflow.dto import ( - EdgeDataDTO, - EndCallNodeData, - EndCallRFNode, - Position, - ReactFlowDTO, - RFEdgeDTO, - StartCallNodeData, - StartCallRFNode, -) -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager -from api.services.workflow.workflow import WorkflowGraph from pipecat.frames.frames import ( Frame, LLMContextFrame, @@ -42,10 +27,25 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMAssistantAggregatorParams, LLMContextAggregatorPair, ) -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams +from api.services.pipecat.recording_audio_cache import RecordingAudio +from api.services.workflow.dto import ( + EdgeDataDTO, + EndCallNodeData, + EndCallRFNode, + Position, + ReactFlowDTO, + RFEdgeDTO, + StartCallNodeData, + StartCallRFNode, +) +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager +from api.services.workflow.workflow import WorkflowGraph +from pipecat.tests import MockLLMService, MockTTSService + # ─── Constants ────────────────────────────────────────────────── START_PROMPT = "Start Call System Prompt" diff --git a/api/tests/test_tts_endframe_with_audio_write_failure.py b/api/tests/test_tts_endframe_with_audio_write_failure.py index c237cdf..56f9ac6 100644 --- a/api/tests/test_tts_endframe_with_audio_write_failure.py +++ b/api/tests/test_tts_endframe_with_audio_write_failure.py @@ -32,12 +32,6 @@ import asyncio from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.pipecat_engine_variable_extractor import ( - VariableExtractionManager, -) -from api.services.workflow.workflow import WorkflowGraph from pipecat.frames.frames import LLMContextFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner @@ -48,7 +42,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMContextAggregatorPair, LLMUserAggregatorParams, ) -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams from pipecat.turns.user_mute import ( @@ -57,6 +50,13 @@ from pipecat.turns.user_mute import ( ) from pipecat.utils.enums import EndTaskReason +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.pipecat_engine_variable_extractor import ( + VariableExtractionManager, +) +from api.services.workflow.workflow import WorkflowGraph +from pipecat.tests import MockLLMService, MockTTSService + async def create_test_pipeline_with_failing_transport( workflow: WorkflowGraph, diff --git a/api/tests/test_unregistered_function_call.py b/api/tests/test_unregistered_function_call.py index 4cd8931..24ed9a1 100644 --- a/api/tests/test_unregistered_function_call.py +++ b/api/tests/test_unregistered_function_call.py @@ -1,7 +1,6 @@ """Tests for LLM behavior when calling an unregistered function.""" import pytest - from pipecat.frames.frames import ( FunctionCallInProgressFrame, FunctionCallResultFrame, @@ -13,6 +12,7 @@ from pipecat.frames.frames import ( ) from pipecat.pipeline.pipeline import Pipeline from pipecat.processors.aggregators.llm_context import LLMContext + from pipecat.tests import MockLLMService, run_test diff --git a/api/tests/test_user_idle_handler.py b/api/tests/test_user_idle_handler.py index 916878d..47d8eee 100644 --- a/api/tests/test_user_idle_handler.py +++ b/api/tests/test_user_idle_handler.py @@ -13,9 +13,6 @@ import asyncio from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.workflow import WorkflowGraph from pipecat.frames.frames import ( BotStoppedSpeakingFrame, Frame, @@ -35,7 +32,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMUserAggregatorParams, ) from pipecat.processors.frame_processor import FrameDirection, FrameProcessor -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams from pipecat.turns.user_mute import ( @@ -47,6 +43,10 @@ from pipecat.turns.user_stop import ExternalUserTurnStopStrategy from pipecat.turns.user_turn_strategies import UserTurnStrategies from pipecat.utils.time import time_now_iso8601 +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.workflow import WorkflowGraph +from pipecat.tests import MockLLMService, MockTTSService + class UserSpeechInjector(FrameProcessor): """Processor that injects user speaking frames after the bot finishes speaking. diff --git a/api/tests/test_user_muting_during_bot_speech.py b/api/tests/test_user_muting_during_bot_speech.py index 17c0f65..b055385 100644 --- a/api/tests/test_user_muting_during_bot_speech.py +++ b/api/tests/test_user_muting_during_bot_speech.py @@ -15,12 +15,6 @@ from typing import List from unittest.mock import AsyncMock, patch import pytest - -from api.services.workflow.pipecat_engine import PipecatEngine -from api.services.workflow.pipecat_engine_variable_extractor import ( - VariableExtractionManager, -) -from api.services.workflow.workflow import WorkflowGraph from pipecat.frames.frames import ( BotStartedSpeakingFrame, BotStoppedSpeakingFrame, @@ -41,7 +35,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMUserAggregatorParams, ) from pipecat.processors.frame_processor import FrameDirection, FrameProcessor -from pipecat.tests import MockLLMService, MockTTSService from pipecat.tests.mock_transport import MockTransport from pipecat.transports.base_transport import TransportParams from pipecat.turns.user_mute import ( @@ -51,6 +44,13 @@ from pipecat.turns.user_mute import ( from pipecat.turns.user_turn_strategies import ExternalUserTurnStrategies from pipecat.utils.time import time_now_iso8601 +from api.services.workflow.pipecat_engine import PipecatEngine +from api.services.workflow.pipecat_engine_variable_extractor import ( + VariableExtractionManager, +) +from api.services.workflow.workflow import WorkflowGraph +from pipecat.tests import MockLLMService, MockTTSService + class BotSpeakingObserverProcessor(FrameProcessor): """Observer that records mute status when bot speaking events flow upstream. diff --git a/api/tests/test_voicemail_detector.py b/api/tests/test_voicemail_detector.py index f2e5141..0677c29 100644 --- a/api/tests/test_voicemail_detector.py +++ b/api/tests/test_voicemail_detector.py @@ -8,7 +8,6 @@ incoming speech as CONVERSATION or VOICEMAIL and how the main LLM responds. import asyncio import pytest - from pipecat.extensions.voicemail.voicemail_detector import VoicemailDetector from pipecat.frames.frames import ( EndTaskFrame, @@ -27,7 +26,6 @@ from pipecat.processors.aggregators.llm_response_universal import ( LLMUserAggregatorParams, ) from pipecat.processors.frame_processor import FrameDirection, FrameProcessor -from pipecat.tests import MockLLMService from pipecat.turns.user_start import ( TranscriptionUserTurnStartStrategy, VADUserTurnStartStrategy, @@ -38,6 +36,8 @@ from pipecat.turns.user_stop import ( from pipecat.turns.user_turn_strategies import UserTurnStrategies from pipecat.utils.time import time_now_iso8601 +from pipecat.tests import MockLLMService + class FrameInjector(FrameProcessor): """Simple processor that can inject frames into the pipeline.""" diff --git a/docs/contribution/setup.mdx b/docs/contribution/setup.mdx index 7054883..6dc95d0 100644 --- a/docs/contribution/setup.mdx +++ b/docs/contribution/setup.mdx @@ -18,9 +18,9 @@ All commands below are shown for **macOS / Linux**. Expand the **Windows** tab f ### Steps 1. Fork the Dograh repository by going to https://github.com/dograh-hq/dograh -2. Clone the forked repository on your machine +2. Clone the forked repository on your machine (use `--recurse-submodules` so the pipecat submodule is pulled in too) ``` -git clone https://github.com//dograh +git clone --recurse-submodules https://github.com//dograh cd dograh ``` 3. Create a python virtual environment @@ -34,19 +34,15 @@ python -m venv venv .\venv\Scripts\Activate.ps1 ``` -4. Install the requirements -``` -pip install -r api/requirements.txt -``` -5. Ensure you are on right version of Node.js using `node --version` +4. Ensure you are on right version of Node.js using `node --version` ``` nvm use 24 ``` -6. Install UI dependencies +5. Install UI dependencies ``` cd ui && npm install && cd .. ``` -7. Start local docker services +6. Start local docker services Please ensure you dont have any other instance of conflicting services running by checking `docker ps` ``` docker compose -f docker-compose-local.yaml up -d @@ -59,7 +55,7 @@ CONTAINER ID IMAGE COMMAND CREATED STATUS 6c7cb8afdf18 redis:7 "docker-entrypoint.s…" 18 seconds ago Up 18 seconds (healthy) 0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp dograh-redis-1 a57e3e92b02c minio/minio "/usr/bin/docker-ent…" 18 seconds ago Up 18 seconds (healthy) 127.0.0.1:9000-9001->9000-9001/tcp dograh-minio-1 ``` -8. Setup environment variables +7. Setup environment variables ```bash macOS/Linux cp api/.env.example api/.env && cp ui/.env.example ui/.env @@ -69,16 +65,24 @@ Copy-Item api/.env.example api/.env Copy-Item ui/.env.example ui/.env ``` -9. Setup pipecat git submodule +8. Install Python requirements. The script initializes the pipecat submodule, installs `api/requirements.txt`, and installs pipecat with the required extras. Add the dev flag if you also want the pipecat dev dependency group (pytest, ruff, pre-commit, etc.). ```bash macOS/Linux -bash scripts/setup_pipecat.sh +# Default (runtime only) +bash scripts/setup_requirements.sh + +# Include pipecat dev dependencies +bash scripts/setup_requirements.sh --dev ``` ```powershell Windows -.\scripts\setup_pipecat.ps1 +# Default (runtime only) +.\scripts\setup_requirements.ps1 + +# Include pipecat dev dependencies +.\scripts\setup_requirements.ps1 -Dev ``` -10. Start backend services +9. Start backend services ```bash macOS/Linux bash scripts/start_services_dev.sh @@ -105,11 +109,11 @@ tail -f logs/latest/*.log Get-Content logs/latest/*.log -Wait ``` -11. Start the UI +10. Start the UI ``` cd ui && npm run dev ``` -12. You should be able to open the application on `localhost:3000` now +11. You should be able to open the application on `localhost:3000` now ### Next Steps We ship with AGENTS.md and CLAUDE.md which will help the Coding Agents get started quickly with the codebase. This should help your favourite coding agents to be able to navigate the codebase quickly and you can make changes to it and suit your specification better. diff --git a/docs/deployment/introduction.mdx b/docs/deployment/introduction.mdx index f8a328c..23d11fc 100644 --- a/docs/deployment/introduction.mdx +++ b/docs/deployment/introduction.mdx @@ -8,5 +8,5 @@ You can deploy Dograh Platform using Docker on a remote server using Docker, eit - [Docker](docker) - [Custom Domain](custom-domain) -- [Web Widget](web-widget) +- [Add to Website](web-widget) - [Heroku](heroku) diff --git a/docs/deployment/web-widget.mdx b/docs/deployment/web-widget.mdx index 5cd7fab..6059b53 100644 --- a/docs/deployment/web-widget.mdx +++ b/docs/deployment/web-widget.mdx @@ -1,11 +1,11 @@ --- -title: Web Widget -description: You can deploy and embed a Voice Agent that you create on Dograh on any Website or Mobile App, where the visitor of the website can interact with your Voice Agent. +title: Add to Website +description: Add your Dograh voice agent to any website so visitors can talk to it. --- -### How to deploy +### How to add it -You can embed your Voice Agent on any external website using the Deploy Agent dialog in your agent's settings. +Add your voice agent to any website using the Deploy Agent dialog in your agent's settings. Step 1: Open the agent settings by clicking the gear icon in the top-right of the agent editor. @@ -15,10 +15,212 @@ Step 2: Scroll to the **Deployment** section and click **Configure Embed**. ![Go to Deployment](../images/go-to-deployment.png) -Step 3: Enable embedding, add your website's domain to **Allowed Domains**, choose either **Floating Widget** or **Inline Component**, customize the button (position, color, text), and click **Save Configurations**. +Step 3: Enable embedding, add your website's domain to **Allowed Domains**, choose **Floating Widget**, **Inline Component**, or **Headless (Bring Your Own UI)**, customize the button (position, color, text) if applicable, and click **Save Configurations**. ![Save configurations](../images/save-configurations.png) Step 4: Copy the generated embed code and paste it into your web page to test your agent. ![Copy deployment code](../images/copy-deployment-code.png) + +## Embed modes + +| Mode | What it renders | When to use | +| --------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| **Floating Widget** | A pill-shaped CTA button anchored to a corner of the page. | You want a turn-key chat-bubble experience that doesn't disturb your existing layout. | +| **Inline Component** | A panel rendered inside a `
` that you place in your page. | You want the agent embedded in a specific section (landing-page hero, support tab, etc.). | +| **Headless** | No UI. Only the audio pipeline plus a JavaScript API on `window.DograhWidget`. | You want full control over the UI — your own buttons, design system, framework state, animations. | + +## Prerequisites + +These apply to all three modes: + +- Serve your page over **HTTPS** or from `http://localhost`. Browsers refuse microphone access on plain HTTP origins or `file://`. +- If you set **Allowed Domains** in the dashboard, include your test origin (e.g. `localhost`) — otherwise the widget's config and signaling requests are rejected. Leave the list empty to allow all domains. +- The embed snippet you copy from the dashboard is a single ` +``` + +### React + TypeScript + +```tsx +import { useEffect, useState } from 'react'; + +type CallStatus = 'idle' | 'connecting' | 'connected' | 'failed'; + +declare global { + interface Window { + DograhWidget: { + start: () => void; + end: () => void; + onStatusChange: (cb: (status: CallStatus, text?: string, subtext?: string) => void) => void; + onError: (cb: (err: Error) => void) => void; + }; + } +} + +export function TalkButton() { + const [status, setStatus] = useState('idle'); + + useEffect(() => { + window.DograhWidget.onStatusChange((s) => setStatus(s)); + window.DograhWidget.onError((err) => console.error('Dograh error:', err.message)); + }, []); + + const isLive = status === 'connected' || status === 'connecting'; + const label = { idle: 'Talk to AI', connecting: 'Connecting…', connected: 'End Call', failed: 'Retry' }[status]; + + return ( + + ); +} +``` + + +`start()` must run inside a real user-gesture handler (`click`, `touchend`, etc.). Browsers refuse to grant microphone access to scripts that request it outside of one — calling `start()` from a `setTimeout` or on page load will fail with a permission error. + + +## Lifecycle callbacks (all modes) + +The `on*` callbacks in the [Headless JavaScript API](#javascript-api) work in **all three embed modes**, not just Headless. Use them for analytics or to trigger UI in the host page even when the widget is rendering its own UI (Floating or Inline). + +```js +window.DograhWidget.onCallConnected(({ agentId, workflowRunId }) => { + analytics.track('voice_call_started', { agentId, workflowRunId }); +}); + +window.DograhWidget.onCallDisconnected(({ workflowRunId, durationSeconds }) => { + analytics.track('voice_call_ended', { workflowRunId, durationSeconds }); +}); +``` + +`onCallConnected` and `onCallDisconnected` only fire when the call actually establishes a media connection — failed-to-connect attempts (e.g. denied mic, network failure) don't trigger them, so analytics stay clean. diff --git a/docs/images/copy-deployment-code.png b/docs/images/copy-deployment-code.png index 5178390..87c8b0d 100644 Binary files a/docs/images/copy-deployment-code.png and b/docs/images/copy-deployment-code.png differ diff --git a/docs/images/floating-widget-example.png b/docs/images/floating-widget-example.png new file mode 100644 index 0000000..d72d5d2 Binary files /dev/null and b/docs/images/floating-widget-example.png differ diff --git a/docs/images/go-to-deployment.png b/docs/images/go-to-deployment.png index 25d49b6..3cf692d 100644 Binary files a/docs/images/go-to-deployment.png and b/docs/images/go-to-deployment.png differ diff --git a/docs/images/headless-widget-example.png b/docs/images/headless-widget-example.png new file mode 100644 index 0000000..80e1161 Binary files /dev/null and b/docs/images/headless-widget-example.png differ diff --git a/docs/images/inline-widget-example.png b/docs/images/inline-widget-example.png new file mode 100644 index 0000000..4dae9bf Binary files /dev/null and b/docs/images/inline-widget-example.png differ diff --git a/docs/images/open-settings.png b/docs/images/open-settings.png index b17e712..0f4a89c 100644 Binary files a/docs/images/open-settings.png and b/docs/images/open-settings.png differ diff --git a/docs/images/save-configurations.png b/docs/images/save-configurations.png index 12feda6..6a00589 100644 Binary files a/docs/images/save-configurations.png and b/docs/images/save-configurations.png differ diff --git a/scripts/setup_pipecat.ps1 b/scripts/setup_pipecat.ps1 deleted file mode 100644 index f831319..0000000 --- a/scripts/setup_pipecat.ps1 +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env pwsh -# Setup script for using pipecat as a git submodule (Windows) - -$ErrorActionPreference = 'Stop' - -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$BaseDir = Split-Path -Parent $ScriptDir -Set-Location $BaseDir - -Write-Host "Setting up pipecat as a git submodule..." - -# Initialize and update submodules -Write-Host "Initializing git submodules..." -git submodule update --init --recursive - -# Install dograh API requirements first so pipecat's extras win on any -# shared transitive dependencies (matches api/Dockerfile and CI workflow). -Write-Host "Installing dograh API requirements..." -pip install -r api/requirements.txt - -# Install pipecat in editable mode with all extras -Write-Host "Installing pipecat dependencies..." -pip install -e './pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]' - -Write-Host "Setup complete! Pipecat is now available as a git submodule." diff --git a/scripts/setup_pipecat.sh b/scripts/setup_pipecat.sh deleted file mode 100755 index cf4cc27..0000000 --- a/scripts/setup_pipecat.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Setup script for using pipecat as a git submodule - -# Get the project root directory (parent of scripts) -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -DOGRAH_DIR="$(dirname "$SCRIPT_DIR")" - -cd "$DOGRAH_DIR" - -echo "Setting up pipecat as a git submodule..." - -# Initialize and update submodules -echo "Initializing git submodules..." -git submodule update --init --recursive - -# Install dograh API requirements first so pipecat's extras win on any -# shared transitive dependencies (matches api/Dockerfile and CI workflow). -echo "Installing dograh API requirements..." -pip install -r api/requirements.txt - -# Install pipecat in editable mode with all extras -echo "Installing pipecat dependencies..." -pip install -e ./pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb] - -echo "Setup complete! Pipecat is now available as a git submodule." \ No newline at end of file diff --git a/scripts/setup_requirements.ps1 b/scripts/setup_requirements.ps1 new file mode 100644 index 0000000..c71dba6 --- /dev/null +++ b/scripts/setup_requirements.ps1 @@ -0,0 +1,48 @@ +#!/usr/bin/env pwsh +# Setup script for using pipecat as a git submodule (Windows). +# +# Usage: +# ./scripts/setup_requirements.ps1 # default: install runtime deps +# ./scripts/setup_requirements.ps1 -Dev # also install pipecat dev deps; +# # skips git submodule update (CI +# # already checks out submodules). + +[CmdletBinding()] +param( + [switch]$Dev +) + +$ErrorActionPreference = 'Stop' + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$BaseDir = Split-Path -Parent $ScriptDir +Set-Location $BaseDir + +Write-Host "Setting up pipecat as a git submodule..." + +if (-not $Dev) { + Write-Host "Initializing git submodules..." + git submodule update --init --recursive +} + +# Install dograh API requirements first so pipecat's extras win on any +# shared transitive dependencies (matches api/Dockerfile and CI workflow). +Write-Host "Installing dograh API requirements..." +pip install -r api/requirements.txt + +if ($Dev) { + Write-Host "Installing dograh API dev requirements..." + pip install -r api/requirements.dev.txt +} + +# Install pipecat in editable mode with all extras +Write-Host "Installing pipecat dependencies..." +pip install -e './pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]' + +if ($Dev) { + Write-Host "Installing pipecat dev dependencies..." + pip install --upgrade pip + pip install --group pipecat/pyproject.toml:dev +} + +Write-Host "Setup complete! Requirements are installed." diff --git a/scripts/setup_requirements.sh b/scripts/setup_requirements.sh new file mode 100755 index 0000000..f38ed38 --- /dev/null +++ b/scripts/setup_requirements.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Setup script for using pipecat as a git submodule. +# +# Usage: +# ./scripts/setup_requirements.sh # default: install runtime deps +# ./scripts/setup_requirements.sh --dev # also install pipecat dev deps; +# # skips git submodule update (CI +# # already checks out submodules). + +set -euo pipefail + +DEV_MODE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --dev) + DEV_MODE=1 + shift + ;; + *) + echo "Unknown argument: $1" >&2 + echo "Usage: $0 [--dev]" >&2 + exit 1 + ;; + esac +done + +# Get the project root directory (parent of scripts) +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +DOGRAH_DIR="$(dirname "$SCRIPT_DIR")" + +cd "$DOGRAH_DIR" + +echo "Setting up pipecat as a git submodule..." + +if [ "$DEV_MODE" -eq 0 ]; then + echo "Initializing git submodules..." + git submodule update --init --recursive +fi + +# Install dograh API requirements first so pipecat's extras win on any +# shared transitive dependencies (matches api/Dockerfile and CI workflow). +echo "Installing dograh API requirements..." +pip install -r api/requirements.txt + +if [ "$DEV_MODE" -eq 1 ]; then + echo "Installing dograh API dev requirements..." + pip install -r api/requirements.dev.txt +fi + +# Install pipecat in editable mode with all extras +echo "Installing pipecat dependencies..." +pip install -e ./pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb] + +if [ "$DEV_MODE" -eq 1 ]; then + echo "Installing pipecat dev dependencies..." + pip install --upgrade pip + pip install --group pipecat/pyproject.toml:dev +fi + +echo "Setup complete! Requirements are installed." diff --git a/ui/public/embed/dograh-widget.js b/ui/public/embed/dograh-widget.js index 647a4be..9a26620 100644 --- a/ui/public/embed/dograh-widget.js +++ b/ui/public/embed/dograh-widget.js @@ -33,6 +33,8 @@ callbacks: { onReady: null, onCallStart: null, + onCallConnected: null, + onCallDisconnected: null, onCallEnd: null, onError: null, onStatusChange: null @@ -114,7 +116,7 @@ containerId: configData.settings?.containerId || 'dograh-inline-container', position: configData.position || DEFAULT_CONFIG.position, buttonColor: configData.settings?.buttonColor || '#10b981', - buttonText: configData.settings?.buttonText || 'Start Call', + buttonText: configData.settings?.buttonText || 'Talk to Agent', callToActionText: configData.settings?.callToActionText || 'Click to start voice conversation', autoStart: configData.auto_start || false }; @@ -125,13 +127,14 @@ state.isInitialized = true; - // Load styles - injectStyles(); - // Create widget UI based on mode if (state.config.embedMode === 'inline') { + injectStyles(); createInlineWidget(); + } else if (state.config.embedMode === 'headless') { + createHeadlessWidget(); } else { + injectStyles(); createFloatingWidget(); } @@ -192,68 +195,43 @@ left: 20px; } - .dograh-widget-button { - color: white; - border: none; - border-radius: 50%; - width: 60px; - height: 60px; - cursor: pointer; - display: flex; + .dograh-widget-cta { + display: inline-flex; align-items: center; - justify-content: center; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - transition: all 0.3s ease; + gap: 8px; + padding: 12px 20px; + border: none; + border-radius: 9999px; + color: #ffffff; + font-size: 14px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; + max-width: calc(100vw - 40px); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2); + transition: filter 150ms ease, transform 100ms ease, box-shadow 200ms ease; + animation: dograh-cta-in 220ms ease-out; } - .dograh-widget-button:hover { - transform: scale(1.1); - box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25); + .dograh-widget-cta:hover { + filter: brightness(1.08); + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28); + } + .dograh-widget-cta:active { transform: scale(0.98); } + + .dograh-widget-cta.dograh-state-connecting { background: #f59e0b !important; animation: dograh-pulse 1.6s infinite; } + .dograh-widget-cta.dograh-state-connected { background: #ef4444 !important; } + .dograh-widget-cta.dograh-state-failed { background: #ef4444 !important; opacity: 0.85; } + + @keyframes dograh-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } } - .dograh-widget-button:active { - transform: scale(0.95); + @keyframes dograh-cta-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } } - - /* Green button for idle/ready state */ - .dograh-widget-button-idle { - background: #10b981; - } - - .dograh-widget-button-idle:hover { - background: #059669; - } - - /* Orange button for connecting state */ - .dograh-widget-button-connecting { - background: #f59e0b; - animation: pulse 2s infinite; - } - - /* Red button for connected state (to end call) */ - .dograh-widget-button-connected { - background: #ef4444; - } - - .dograh-widget-button-connected:hover { - background: #dc2626; - } - - /* Red button for failed state */ - .dograh-widget-button-failed { - background: #ef4444; - opacity: 0.8; - } - - @keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.6; - } - } - `; const styleSheet = document.createElement('style'); @@ -262,39 +240,81 @@ document.head.appendChild(styleSheet); } + function ctaLabelForStatus(status) { + switch (status) { + case 'connecting': return 'Connecting…'; + case 'connected': return 'End Call'; + case 'failed': return 'Retry'; + default: return state.config.buttonText || 'Talk to Agent'; + } + } + /** - * Create floating widget UI (simplified - no modal) + * Create floating widget UI — a single CTA pill button anchored to the + * configured corner of the viewport. */ function createFloatingWidget() { - // Create container const container = document.createElement('div'); container.className = `dograh-widget-container ${state.config.position}`; - container.id = 'dograh-widget'; + container.id = 'dograh-widget-root'; - // Create button (configured color to start, red to end) - const button = document.createElement('button'); - button.className = 'dograh-widget-button dograh-widget-button-idle'; - button.id = 'dograh-widget-button'; - button.style.backgroundColor = state.config.buttonColor; - button.innerHTML = ` - - - - `; - button.onclick = toggleCall; - - // Create hidden audio element const audio = document.createElement('audio'); audio.id = 'dograh-widget-audio'; audio.autoplay = true; audio.style.display = 'none'; - - // Append elements - container.appendChild(button); container.appendChild(audio); - document.body.appendChild(container); + state.audioElement = audio; - // Store audio element reference + document.body.appendChild(container); + renderFloating(); + } + + /** + * Render the floating CTA pill. Re-renders preserve the hidden audio + * element so an in-progress call is not interrupted on status changes. + */ + function renderFloating() { + const container = document.getElementById('dograh-widget-root'); + if (!container) return; + + Array.from(container.children).forEach((child) => { + if (child !== state.audioElement) container.removeChild(child); + }); + + const status = state.connectionStatus || 'idle'; + + const button = document.createElement('button'); + button.id = 'dograh-widget-cta'; + button.type = 'button'; + button.className = `dograh-widget-cta dograh-state-${status}`; + // Idle uses configured color; status states use CSS-defined colors. + if (status === 'idle') { + button.style.backgroundColor = state.config.buttonColor; + } + button.innerHTML = ` + + + + + + + + `; + button.querySelector('span').textContent = ctaLabelForStatus(status); + button.onclick = toggleCall; + + container.appendChild(button); + } + + /** + * Create headless widget (no UI — host page drives everything via window.DograhWidget API) + */ + function createHeadlessWidget() { + const audio = document.createElement('audio'); + audio.id = 'dograh-widget-audio'; + audio.autoplay = true; + audio.style.display = 'none'; + document.body.appendChild(audio); state.audioElement = audio; } @@ -309,30 +329,9 @@ } } - /** - * Update floating widget button appearance - */ function updateFloatingButton(status) { - const button = document.getElementById('dograh-widget-button'); - if (!button) return; - - // Remove all status classes - button.classList.remove('dograh-widget-button-idle', 'dograh-widget-button-connecting', 'dograh-widget-button-connected', 'dograh-widget-button-failed'); - - // Add current status class - button.classList.add(`dograh-widget-button-${status}`); - - // Apply configured color only for idle state, let CSS handle other states - button.style.backgroundColor = status === 'idle' ? state.config.buttonColor : ''; - - // Update title attribute for tooltip - const titles = { - idle: 'Start Call', - connecting: 'Connecting...', - connected: 'End Call', - failed: 'Retry Call' - }; - button.title = titles[status] || 'Voice Call'; + state.connectionStatus = status; + renderFloating(); } /** @@ -582,6 +581,10 @@ // Use appropriate update function based on mode if (state.config.embedMode === 'inline') { updateInlineStatus(status, text, subtext); + } else if (state.config.embedMode === 'headless') { + if (state.callbacks.onStatusChange) { + state.callbacks.onStatusChange(status, text, subtext); + } } else { updateFloatingButton(status); } @@ -610,7 +613,6 @@ async function startCall() { updateStatus('connecting', 'Connecting...', 'Please wait while we establish the connection'); - // Trigger call start callback if (state.callbacks.onCallStart) { state.callbacks.onCallStart(); } @@ -768,9 +770,18 @@ console.log('ICE connection state:', state.pc.iceConnectionState); if (state.pc.iceConnectionState === 'connected' || state.pc.iceConnectionState === 'completed') { + const wasAlreadyConnected = state.callStartedAt !== null; updateStatus('connected', 'Connected', 'Your voice call is now active'); - state.callStartedAt = Date.now(); - emitMessage('dograh:call_started', {}); + if (!wasAlreadyConnected) { + state.callStartedAt = Date.now(); + if (state.callbacks.onCallConnected) { + state.callbacks.onCallConnected({ + agentId: state.config.workflowId || null, + token: state.config.token || null, + workflowRunId: state.workflowRunId || null + }); + } + } } else if (state.pc.iceConnectionState === 'failed' || state.pc.iceConnectionState === 'disconnected') { updateStatus('failed', 'Connection lost', 'The call has been disconnected'); stopCall(); @@ -903,16 +914,21 @@ * Stop voice call */ function stopCall() { - // Emit end message before clearing state so identifiers are still available - const durationSeconds = state.callStartedAt - ? Math.round((Date.now() - state.callStartedAt) / 1000) - : 0; - emitMessage('dograh:call_ended', { durationSeconds }); + // Fire onCallDisconnected only if the call had actually connected, with + // identifiers and duration. Must run before we clear callStartedAt. + if (state.callStartedAt && state.callbacks.onCallDisconnected) { + const durationSeconds = Math.round((Date.now() - state.callStartedAt) / 1000); + state.callbacks.onCallDisconnected({ + agentId: state.config.workflowId || null, + token: state.config.token || null, + workflowRunId: state.workflowRunId || null, + durationSeconds + }); + } state.callStartedAt = null; updateStatus('idle', 'Call ended', 'Click below to start a new call'); - // Trigger call end callback if (state.callbacks.onCallEnd) { state.callbacks.onCallEnd(); } @@ -949,22 +965,6 @@ setTimeout(() => startCall(), 500); } - /** - * Emit a postMessage event to the host window - * Allows the embedding website to listen for agent lifecycle events via: - * window.addEventListener('message', (event) => { ... }) - */ - function emitMessage(eventType, detail) { - const message = { - type: eventType, - agentId: state.config.workflowId || null, - token: state.config.token || null, - workflowRunId: state.workflowRunId || null, - ...detail - }; - window.postMessage(message, '*'); - } - /** * Generate unique peer ID */ @@ -993,6 +993,8 @@ getState: () => state, onReady: (callback) => { state.callbacks.onReady = callback; }, onCallStart: (callback) => { state.callbacks.onCallStart = callback; }, + onCallConnected: (callback) => { state.callbacks.onCallConnected = callback; }, + onCallDisconnected: (callback) => { state.callbacks.onCallDisconnected = callback; }, onCallEnd: (callback) => { state.callbacks.onCallEnd = callback; }, onError: (callback) => { state.callbacks.onError = callback; }, onStatusChange: (callback) => { state.callbacks.onStatusChange = callback; }, @@ -1018,6 +1020,8 @@ // Set callbacks if provided if (options.onReady) state.callbacks.onReady = options.onReady; if (options.onCallStart) state.callbacks.onCallStart = options.onCallStart; + if (options.onCallConnected) state.callbacks.onCallConnected = options.onCallConnected; + if (options.onCallDisconnected) state.callbacks.onCallDisconnected = options.onCallDisconnected; if (options.onCallEnd) state.callbacks.onCallEnd = options.onCallEnd; if (options.onError) state.callbacks.onError = options.onError; if (options.onStatusChange) state.callbacks.onStatusChange = options.onStatusChange; diff --git a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx index ace9e02..b2e54b2 100644 --- a/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx +++ b/ui/src/app/workflow/[workflowId]/components/EmbedDialog.tsx @@ -1,4 +1,4 @@ -import { Check, Copy, Loader2, Plus, Rocket, Trash2 } from "lucide-react"; +import { Check, Copy, Loader2, Mic, Plus, Rocket, Trash2 } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { @@ -61,9 +61,9 @@ export function EmbedDialog({ const [isEnabled, setIsEnabled] = useState(false); const [domains, setDomains] = useState([]); const [newDomain, setNewDomain] = useState(""); - const [embedMode, setEmbedMode] = useState<"floating" | "inline">("floating"); + const [embedMode, setEmbedMode] = useState<"floating" | "inline" | "headless">("floating"); const [position, setPosition] = useState("bottom-right"); - const [buttonText, setButtonText] = useState("Start Call"); + const [buttonText, setButtonText] = useState("Talk to Agent"); const [buttonColor, setButtonColor] = useState("#10b981"); const [callToActionText, setCallToActionText] = useState("Click to start voice conversation"); @@ -81,9 +81,9 @@ export function EmbedDialog({ // Load settings if (response.data.settings) { const settings = response.data.settings as Record; - setEmbedMode((settings.embedMode as "floating" | "inline") || "floating"); + setEmbedMode((settings.embedMode as "floating" | "inline" | "headless") || "floating"); setPosition(settings.position || "bottom-right"); - setButtonText(settings.buttonText || "Start Call"); + setButtonText(settings.buttonText || "Talk to Agent"); setButtonColor(settings.buttonColor || "#10b981"); setCallToActionText(settings.callToActionText || "Click to start voice conversation"); } @@ -266,7 +266,7 @@ export function EmbedDialog({ {/* Embed Mode Selection */}
-
+
+
@@ -306,26 +322,40 @@ export function EmbedDialog({
- {/* Shared: Button Color */} -
- -
- setButtonColor(e.target.value)} - className="w-14 h-10 cursor-pointer" - /> - setButtonColor(e.target.value)} - placeholder="#10b981" - className="flex-1" - /> + {/* Shared: Button Text + Button Color (skipped in headless — host renders its own UI) */} + {embedMode !== "headless" && ( +
+
+ + setButtonText(e.target.value)} + placeholder="Talk to Agent" + maxLength={40} + /> +
+
+ +
+ setButtonColor(e.target.value)} + className="w-14 h-10 cursor-pointer" + /> + setButtonColor(e.target.value)} + placeholder="#10b981" + className="flex-1" + /> +
+
-
+ )} {/* Floating mode: Position */} {embedMode === "floating" && ( @@ -345,52 +375,29 @@ export function EmbedDialog({
)} - {/* Inline mode: Button Text, CTA Text */} + {/* Inline mode: Call to Action Text */} {embedMode === "inline" && ( - <> -
-
- - setButtonText(e.target.value)} - placeholder="Start Call" - /> -
-
- - setCallToActionText(e.target.value)} - placeholder="Click to start voice conversation" - /> -
-
- +
+ + setCallToActionText(e.target.value)} + placeholder="Click to start voice conversation" + /> +
)} - {/* Preview */} - {embedMode === "floating" ? ( -
-
+
+ + {buttonText || "Talk to Agent"} +
) : (
@@ -410,6 +417,64 @@ export function EmbedDialog({
)} + {/* Headless mode: Integration Instructions */} + {embedMode === "headless" && ( +
+
+

Integration Instructions

+
    +
  • • Add the embed script tag to your page (see below).
  • +
  • • The widget renders no UI — render your own buttons.
  • +
  • • Call window.DograhWidget.start() to begin a call.
  • +
  • • Call window.DograhWidget.end() to end it.
  • +
  • • Subscribe to onCallStart, onCallEnd, onStatusChange, onError to drive your UI.
  • +
  • start() must run inside a user-gesture handler (click) so the browser grants microphone access.
  • +
+
+ +
+

Example — track status in your own state

+

+ Mirror the call status into a variable you control, then render whatever UI you like from it. The status values are idle, connecting, connected, failed. +

+
+                                                    {`// Vanilla JS — keep your own state, render however you want
+let callStatus = 'idle';
+
+window.DograhWidget?.onStatusChange((status) => {
+  callStatus = status;
+  // ...trigger your render here (re-paint DOM, dispatch event, etc.)
+});
+
+document.getElementById('talk-btn').addEventListener('click', () => {
+  if (callStatus === 'connected' || callStatus === 'connecting') {
+    window.DograhWidget.end();
+  } else {
+    window.DograhWidget.start();
+  }
+});`}
+                                                
+

React:

+
+                                                    {`function TalkButton() {
+  const [status, setStatus] = useState('idle');
+
+  useEffect(() => {
+    window.DograhWidget?.onStatusChange(setStatus);
+  }, []);
+
+  const isLive = status === 'connected' || status === 'connecting';
+  return (
+    
+  );
+}`}
+                                                
+
+
+ )} + {/* Inline mode: Integration Instructions */} {embedMode === "inline" && (