open source

This commit is contained in:
Ajay Raj 2023-03-28 00:15:34 -07:00
commit a93bfc1ec9
61 changed files with 4013 additions and 126 deletions

View file

View file

@ -0,0 +1,17 @@
import logging
import os
from typing import Optional
from redis import Redis
from vocode.streaming.models.telephony import CallConfig
class BaseConfigManager:
def save_config(self, conversation_id: str, config: CallConfig):
raise NotImplementedError
def get_config(self, conversation_id) -> Optional[CallConfig]:
raise NotImplementedError
def delete_config(self, conversation_id):
raise NotImplementedError

View file

@ -0,0 +1,34 @@
import logging
import os
from typing import Optional
from redis import Redis
from vocode.streaming.models.telephony import CallConfig
from vocode.streaming.telephony.config_manager.base_config_manager import (
BaseConfigManager,
)
class RedisConfigManager(BaseConfigManager):
def __init__(self, logger: Optional[logging.Logger] = None):
self.redis = Redis(
host=os.environ.get("REDISHOST", "localhost"),
port=int(os.environ.get("REDISPORT", 6379)),
db=0,
decode_responses=True,
)
self.logger = logger or logging.getLogger(__name__)
def save_config(self, conversation_id: str, config: CallConfig):
self.logger.debug(f"Saving config for {conversation_id}")
self.redis.set(conversation_id, config.json())
def get_config(self, conversation_id) -> Optional[CallConfig]:
self.logger.debug(f"Getting config for {conversation_id}")
raw_config = self.redis.get(conversation_id)
if raw_config:
return CallConfig.parse_raw(self.redis.get(conversation_id))
def delete_config(self, conversation_id):
self.logger.debug(f"Deleting config for {conversation_id}")
self.redis.delete(conversation_id)

View file

@ -0,0 +1,5 @@
from vocode.streaming.models.audio_encoding import AudioEncoding
DEFAULT_SAMPLING_RATE = 8000
DEFAULT_AUDIO_ENCODING = AudioEncoding.MULAW
DEFAULT_CHUNK_SIZE = 20 * 160

View file

@ -0,0 +1,170 @@
from fastapi import WebSocket
import base64
from enum import Enum
import json
import logging
from typing import Optional
from vocode.streaming.agent.base_agent import BaseAgent
from vocode.streaming.factory import (
create_agent,
create_synthesizer,
create_transcriber,
)
from vocode.streaming.streaming_conversation import StreamingConversation
from vocode.streaming.models.telephony import CallConfig, TwilioConfig
from vocode.streaming.output_device.twilio_output_device import TwilioOutputDevice
from vocode.streaming.models.synthesizer import (
AzureSynthesizerConfig,
)
from vocode.streaming.models.transcriber import (
DeepgramTranscriberConfig,
PunctuationEndpointingConfig,
)
from vocode.streaming.synthesizer.azure_synthesizer import AzureSynthesizer
from vocode.streaming.synthesizer.base_synthesizer import BaseSynthesizer
from vocode.streaming.telephony.config_manager.base_config_manager import (
BaseConfigManager,
)
from vocode.streaming.telephony.twilio import create_twilio_client
from vocode.streaming.models.audio_encoding import AudioEncoding
from vocode.streaming.streaming_conversation import StreamingConversation
from vocode.streaming.transcriber.base_transcriber import BaseTranscriber
from vocode.streaming.transcriber.deepgram_transcriber import DeepgramTranscriber
class PhoneCallAction(Enum):
CLOSE_WEBSOCKET = 1
class Call(StreamingConversation):
def __init__(
self,
base_url: str,
config_manager: BaseConfigManager,
agent: BaseAgent,
twilio_config: TwilioConfig,
transcriber: Optional[BaseTranscriber] = None,
synthesizer: Optional[BaseSynthesizer] = None,
twilio_sid=None,
conversation_id: Optional[str] = None,
logger: Optional[logging.Logger] = None,
):
self.base_url = base_url
self.config_manager = config_manager
self.output_device = TwilioOutputDevice()
self.twilio_config = twilio_config
self.twilio_client = create_twilio_client(twilio_config)
super().__init__(
self.output_device,
transcriber
or DeepgramTranscriber(
DeepgramTranscriberConfig(
sampling_rate=8000,
audio_encoding=AudioEncoding.MULAW,
chunk_size=self.CHUNK_SIZE,
model="voicemail",
endpointing_config=PunctuationEndpointingConfig(),
),
logger=logger,
),
agent,
synthesizer
or AzureSynthesizer(
AzureSynthesizerConfig(
sampling_rate=8000, audio_encoding=AudioEncoding.MULAW
)
),
conversation_id=conversation_id,
per_chunk_allowance_seconds=0.01,
logger=logger,
)
self.twilio_sid = twilio_sid
self.latest_media_timestamp = 0
@staticmethod
def from_call_config(
base_url: str,
call_config: CallConfig,
config_manager: BaseConfigManager,
conversation_id: str,
logger: logging.Logger,
):
return Call(
base_url=base_url,
logger=logger,
config_manager=config_manager,
agent=create_agent(call_config.agent_config),
transcriber=create_transcriber(call_config.transcriber_config),
synthesizer=create_synthesizer(call_config.synthesizer_config),
twilio_config=call_config.twilio_config,
twilio_sid=call_config.twilio_sid,
conversation_id=conversation_id,
)
async def attach_ws_and_start(self, ws: WebSocket):
self.logger.debug("Trying to attach WS to outbound call")
self.output_device.ws = ws
self.logger.debug("Attached WS to outbound call")
twilio_call = self.twilio_client.calls(self.twilio_sid).fetch()
if twilio_call.answered_by in ("machine_start", "fax"):
self.logger.info(f"Call answered by {twilio_call.answered_by}")
twilio_call.update(status="completed")
else:
await self.wait_for_twilio_start(ws)
await super().start()
while self.active:
message = await ws.receive_text()
response = await self.handle_ws_message(message)
if response == PhoneCallAction.CLOSE_WEBSOCKET:
break
self.tear_down()
async def wait_for_twilio_start(self, ws: WebSocket):
while True:
message = await ws.receive_text()
if not message:
continue
data = json.loads(message)
if data["event"] == "start":
self.logger.debug(
f"Media WS: Received event '{data['event']}': {message}"
)
self.output_device.stream_sid = data["start"]["streamSid"]
break
async def handle_ws_message(self, message) -> PhoneCallAction:
if message is None:
return PhoneCallAction.CLOSE_WEBSOCKET
data = json.loads(message)
if data["event"] == "media":
media = data["media"]
chunk = base64.b64decode(media["payload"])
if self.latest_media_timestamp + 20 < int(media["timestamp"]):
bytes_to_fill = 8 * (
int(media["timestamp"]) - (self.latest_media_timestamp + 20)
)
self.logger.debug(f"Filling {bytes_to_fill} bytes of silence")
# NOTE: 0xff is silence for mulaw audio
self.receive_audio(b"\xff" * bytes_to_fill)
self.latest_media_timestamp = int(media["timestamp"])
self.receive_audio(chunk)
elif data["event"] == "stop":
self.logger.debug(f"Media WS: Received event 'stop': {message}")
self.logger.debug("Stopping...")
return PhoneCallAction.CLOSE_WEBSOCKET
def end_twilio_call(self) -> bool:
response = self.twilio_client.calls(self.twilio_sid).update(status="completed")
return response.status == "completed"
def mark_terminated(self):
super().mark_terminated()
self.end_twilio_call()
self.config_manager.delete_config(self.id)
def tear_down(self):
self.terminate()

View file

@ -0,0 +1,110 @@
import logging
from typing import Optional
from twilio.rest import Client
from vocode.streaming.models.agent import AgentConfig
from vocode.streaming.models.synthesizer import (
AzureSynthesizerConfig,
SynthesizerConfig,
)
from vocode.streaming.models.telephony import CallConfig, TwilioConfig
from vocode.streaming.models.transcriber import (
DeepgramTranscriberConfig,
PunctuationEndpointingConfig,
TranscriberConfig,
)
from vocode.streaming.telephony.config_manager.base_config_manager import (
BaseConfigManager,
)
from vocode.streaming.telephony.constants import (
DEFAULT_AUDIO_ENCODING,
DEFAULT_CHUNK_SIZE,
DEFAULT_SAMPLING_RATE,
)
from vocode.streaming.telephony.twilio import create_twilio_client
from vocode.streaming.utils import create_conversation_id
class OutboundCall:
def __init__(
self,
base_url: str,
to_phone: str,
from_phone: str,
config_manager: BaseConfigManager,
agent_config: AgentConfig,
twilio_config: TwilioConfig,
transcriber_config: Optional[TranscriberConfig] = None,
synthesizer_config: Optional[SynthesizerConfig] = None,
conversation_id: Optional[str] = None,
logger: Optional[logging.Logger] = None,
):
self.base_url = base_url
self.to_phone = to_phone
self.from_phone = from_phone
self.config_manager = config_manager
self.agent_config = agent_config
self.transcriber_config = transcriber_config or DeepgramTranscriberConfig(
sampling_rate=DEFAULT_SAMPLING_RATE,
audio_encoding=DEFAULT_AUDIO_ENCODING,
chunk_size=DEFAULT_CHUNK_SIZE,
model="voicemail",
endpointing_config=PunctuationEndpointingConfig(),
)
self.synthesizer_config = synthesizer_config or AzureSynthesizerConfig(
sampling_rate=DEFAULT_SAMPLING_RATE, audio_encoding=DEFAULT_AUDIO_ENCODING
)
self.conversation_id = conversation_id or create_conversation_id()
self.logger = logger
self.twilio_config = twilio_config
self.twilio_client = create_twilio_client(twilio_config)
self.twilio_sid = None
def create_twilio_call(
self, to_phone: str, from_phone: str, digits: str = ""
) -> str:
twilio_call = self.twilio_client.calls.create(
url=f"https://{self.base_url}/twiml/initiate_call/{self.conversation_id}",
to=to_phone,
from_=from_phone,
send_digits=digits,
)
return twilio_call.sid
def validate_outbound_call(
self,
to_phone: str,
from_phone: str,
mobile_only: bool = True,
):
if len(to_phone) < 8:
raise ValueError("Invalid 'to' phone")
if not mobile_only:
return
line_type_intelligence = (
self.twilio_client.lookups.v2.phone_numbers(to_phone)
.fetch(fields="line_type_intelligence")
.line_type_intelligence
)
if not line_type_intelligence or (
line_type_intelligence and line_type_intelligence["type"] != "mobile"
):
raise ValueError("Can only call mobile phones")
def start(self):
self.logger.debug("Starting outbound call")
self.validate_outbound_call(self.to_phone, self.from_phone)
self.twilio_sid = self.create_twilio_call(self.to_phone, self.from_phone)
call_config = CallConfig(
transcriber_config=self.transcriber_config,
agent_config=self.agent_config,
synthesizer_config=self.synthesizer_config,
twilio_config=self.twilio_config,
twilio_sid=self.twilio_sid,
)
self.config_manager.save_config(self.conversation_id, call_config)
def end(self):
response = self.twilio_client.calls(self.twilio_sid).update(status="completed")
return response.status == "completed"

View file

@ -0,0 +1,73 @@
import logging
from typing import Optional
from twilio.rest import Client
from vocode.streaming.agent.base_agent import BaseAgent
from vocode.streaming.models.agent import AgentConfig
from vocode.streaming.models.synthesizer import SynthesizerConfig
from vocode.streaming.models.telephony import CallConfig, TwilioConfig
from vocode.streaming.models.transcriber import TranscriberConfig
from vocode.streaming.synthesizer.base_synthesizer import BaseSynthesizer
from vocode.streaming.telephony.config_manager.base_config_manager import (
BaseConfigManager,
)
from vocode.streaming.telephony.conversation.outbound_call import OutboundCall
from vocode.streaming.transcriber.base_transcriber import BaseTranscriber
from vocode.streaming.utils import create_conversation_id
class ZoomDialIn(OutboundCall):
def __init__(
self,
base_url: str,
zoom_number: str,
zoom_meeting_id: str,
zoom_meeting_password: Optional[str],
from_phone: str,
config_manager: BaseConfigManager,
twilio_config: TwilioConfig,
agent_config: AgentConfig,
transcriber_config: TranscriberConfig,
synthesizer_config: SynthesizerConfig,
conversation_id: Optional[str] = None,
logger: Optional[logging.Logger] = None,
):
super().__init__(
base_url=base_url,
to_phone=zoom_number,
from_phone=from_phone,
config_manager=config_manager,
transcriber_config=transcriber_config,
agent_config=agent_config,
synthesizer_config=synthesizer_config,
twilio_config=twilio_config,
conversation_id=conversation_id,
logger=logger,
)
self.zoom_number = zoom_number
self.zoom_meeting_id = zoom_meeting_id
self.zoom_meeting_password = zoom_meeting_password
self.from_phone = from_phone
def start(self):
self.validate_outbound_call(
self.zoom_number,
self.from_phone,
mobile_only=False,
)
digits = f"ww{self.zoom_meeting_id}#"
if self.zoom_meeting_password:
digits += f"wwww*{self.zoom_meeting_password}#"
self.logger.debug("Sending digits %s to the call", digits)
twilio_sid = self.create_twilio_call(
self.zoom_number,
self.from_phone,
digits=digits,
)
call_config = CallConfig(
transcriber_config=self.transcriber_config,
agent_config=self.agent_config,
synthesizer_config=self.synthesizer_config,
twilio_config=self.twilio_config,
twilio_sid=twilio_sid,
)
self.config_manager.save_config(self.conversation_id, call_config)

View file

@ -0,0 +1,62 @@
from fastapi import FastAPI, Response, Form
from typing import Optional
import requests
import uvicorn
import vocode
from vocode.streaming.models.transcriber import TranscriberConfig
from vocode.streaming.models.synthesizer import SynthesizerConfig
from vocode.streaming.models.agent import AgentConfig
from vocode.streaming.models.telephony import (
CreateInboundCall,
TwilioConfig,
TwilioConfig,
)
class InboundCallServer:
def __init__(
self,
agent_config: AgentConfig,
transcriber_config: Optional[TranscriberConfig] = None,
synthesizer_config: Optional[SynthesizerConfig] = None,
response_on_rate_limit: Optional[str] = None,
twilio_config: Optional[TwilioConfig] = None,
):
self.agent_config = agent_config
self.transcriber_config = transcriber_config
self.synthesizer_config = synthesizer_config
self.app = FastAPI()
self.app.post("/vocode")(self.handle_call)
self.response_on_rate_limit = (
response_on_rate_limit
or "The line is really busy right now, check back later!"
)
self.twilio_config = twilio_config
self.vocode_inbound_call_url = f"https://{vocode.base_url}/create_inbound_call"
async def handle_call(self, twilio_sid: str = Form(alias="CallSid")):
response = requests.post(
self.vocode_inbound_call_url,
headers={"Authorization": f"Bearer {vocode.api_key}"},
json=CreateInboundCall(
agent_config=self.agent_config,
twilio_sid=twilio_sid,
transcriber_config=self.transcriber_config,
synthesizer_config=self.synthesizer_config,
twilio_config=self.twilio_config,
).dict(),
)
if response.status_code == 429:
return Response(
f"<Response><Say>{self.response_on_rate_limit}</Say></Response>",
media_type="application/xml",
)
assert response.ok, response.text
return Response(
response.text,
media_type="application/xml",
)
def run(self, host="localhost", port=3000):
uvicorn.run(self.app, host=host, port=port)

View file

@ -0,0 +1,45 @@
from typing import Optional, Union
from vocode.streaming.models.telephony import TwilioConfig
from vocode.streaming.telephony.hosted.inbound_call_server import InboundCallServer
from vocode.streaming.models.agent import (
RESTfulAgentEnd,
RESTfulAgentInput,
RESTfulAgentText,
RESTfulUserImplementedAgentConfig,
)
from vocode.streaming.models.transcriber import (
TranscriberConfig,
)
from vocode.streaming.models.synthesizer import SynthesizerConfig
class InboundCallUserAgentServer(InboundCallServer):
def __init__(
self,
agent_config: RESTfulUserImplementedAgentConfig,
transcriber_config: Optional[TranscriberConfig] = None,
synthesizer_config: Optional[SynthesizerConfig] = None,
response_on_rate_limit: Optional[str] = None,
twilio_config: Optional[TwilioConfig] = None,
):
super().__init__(
agent_config=agent_config,
transcriber_config=transcriber_config,
synthesizer_config=synthesizer_config,
response_on_rate_limit=response_on_rate_limit,
twilio_config=twilio_config,
)
assert isinstance(
agent_config, RESTfulUserImplementedAgentConfig
), "agent_config must be a RESTfulUserImplementedAgentConfig"
self.app.post("/respond")(self.respond_rest)
async def respond(
self, human_input, conversation_id
) -> Union[RESTfulAgentText, RESTfulAgentEnd]:
raise NotImplementedError
async def respond_rest(
self, request: RESTfulAgentInput
) -> Union[RESTfulAgentText, RESTfulAgentEnd]:
return await self.respond(request.human_input, request.conversation_id)

View file

@ -0,0 +1,68 @@
from typing import Optional
import requests
import vocode
from vocode.streaming.models.agent import AgentConfig
from vocode.streaming.models.synthesizer import SynthesizerConfig
from vocode.streaming.models.transcriber import TranscriberConfig
from vocode.streaming.models.telephony import (
CallEntity,
CreateOutboundCall,
EndOutboundCall,
TwilioConfig,
)
class OutboundCall:
def __init__(
self,
recipient: CallEntity,
caller: CallEntity,
agent_config: AgentConfig,
transcriber_config: Optional[TranscriberConfig] = None,
synthesizer_config: Optional[SynthesizerConfig] = None,
conversation_id: Optional[str] = None,
twilio_config: Optional[TwilioConfig] = None,
):
self.recipient = recipient
self.caller = caller
self.agent_config = agent_config
self.transcriber_config = transcriber_config
self.synthesizer_config = synthesizer_config
self.conversation_id = conversation_id
self.twilio_config = twilio_config
self.vocode_create_outbound_call_url = (
f"https://{vocode.base_url}/create_outbound_call"
)
self.vocode_end_outbound_call_url = (
f"https://{vocode.base_url}/end_outbound_call"
)
def start(self) -> str:
response = requests.post(
self.vocode_create_outbound_call_url,
headers={"Authorization": f"Bearer {vocode.api_key}"},
json=CreateOutboundCall(
recipient=self.recipient,
caller=self.caller,
agent_config=self.agent_config,
transcriber_config=self.transcriber_config,
synthesizer_config=self.synthesizer_config,
conversation_id=self.conversation_id,
twilio_config=self.twilio_config,
).dict(),
)
assert response.ok, response.text
data = response.json()
self.conversation_id = data["id"]
def end(self) -> str:
response = requests.post(
self.vocode_end_outbound_call_url,
headers={"Authorization": f"Bearer {vocode.api_key}"},
json=EndOutboundCall(
call_id=self.conversation_id,
twilio_config=self.twilio_config,
).dict(),
)
assert response.ok or response.status_code == 404, response.text

View file

@ -0,0 +1,60 @@
from typing import Optional
import requests
import vocode
from vocode.streaming.models.agent import AgentConfig
from vocode.streaming.models.synthesizer import SynthesizerConfig
from vocode.streaming.models.transcriber import TranscriberConfig
from vocode.streaming.telephony.hosted.outbound_call import OutboundCall
from vocode.streaming.models.telephony import (
CallEntity,
DialIntoZoomCall,
TwilioConfig,
)
class ZoomDialIn(OutboundCall):
def __init__(
self,
recipient: CallEntity,
caller: CallEntity,
zoom_meeting_id: str,
zoom_meeting_password: str,
agent_config: AgentConfig,
transcriber_config: Optional[TranscriberConfig] = None,
synthesizer_config: Optional[SynthesizerConfig] = None,
conversation_id: Optional[str] = None,
twilio_config: Optional[TwilioConfig] = None,
):
super().__init__(
recipient=recipient,
caller=caller,
agent_config=agent_config,
transcriber_config=transcriber_config,
synthesizer_config=synthesizer_config,
conversation_id=conversation_id,
twilio_config=twilio_config,
)
self.zoom_meeting_id = zoom_meeting_id
self.zoom_meeting_password = zoom_meeting_password
self.vocode_zoom_dial_in_url = f"https://{vocode.base_url}/dial_into_zoom_call"
def start(self) -> str:
response = requests.post(
self.vocode_zoom_dial_in_url,
headers={"Authorization": f"Bearer {vocode.api_key}"},
json=DialIntoZoomCall(
recipient=self.recipient,
caller=self.caller,
zoom_meeting_id=self.zoom_meeting_id,
zoom_meeting_password=self.zoom_meeting_password,
agent_config=self.agent_config,
transcriber_config=self.transcriber_config,
synthesizer_config=self.synthesizer_config,
conversation_id=self.conversation_id,
twilio_config=self.twilio_config,
).dict(),
)
assert response.ok, response.text
data = response.json()
self.conversation_id = data["id"]

View file

@ -0,0 +1,143 @@
import logging
from typing import Optional
from fastapi import APIRouter, Form, Response
from pydantic import BaseModel
from vocode.streaming.agent.base_agent import BaseAgent
from vocode.streaming.models.agent import AgentConfig
from vocode.streaming.models.synthesizer import (
AzureSynthesizerConfig,
SynthesizerConfig,
)
from vocode.streaming.models.transcriber import (
DeepgramTranscriberConfig,
PunctuationEndpointingConfig,
TranscriberConfig,
)
from vocode.streaming.synthesizer.base_synthesizer import BaseSynthesizer
from vocode.streaming.telephony.config_manager.base_config_manager import (
BaseConfigManager,
)
from vocode.streaming.telephony.constants import (
DEFAULT_AUDIO_ENCODING,
DEFAULT_CHUNK_SIZE,
DEFAULT_SAMPLING_RATE,
)
from vocode.streaming.telephony.server.router.calls import CallsRouter
from vocode.streaming.telephony.server.router.twiml import TwiMLRouter
from vocode.streaming.models.telephony import (
CallConfig,
CallEntity,
CreateOutboundCall,
CreateInboundCall,
DialIntoZoomCall,
EndOutboundCall,
TwilioConfig,
)
from twilio.rest import Client
from vocode.streaming.telephony.conversation.call import Call
from vocode.streaming.telephony.templates import Templater
from vocode.streaming.transcriber.base_transcriber import BaseTranscriber
from vocode.streaming.utils import create_conversation_id
class InboundCallConfig(BaseModel):
url: str
agent_config: AgentConfig
twilio_config: TwilioConfig
transcriber_config: Optional[TranscriberConfig] = None
synthesizer_config: Optional[SynthesizerConfig] = None
class TelephonyServer:
def __init__(
self,
base_url: str,
config_manager: BaseConfigManager,
inbound_call_configs: list[InboundCallConfig] = [],
logger: Optional[logging.Logger] = None,
):
self.base_url = base_url
self.logger = logger or logging.getLogger(__name__)
self.router = APIRouter()
self.config_manager = config_manager
self.templater = Templater()
self.router.include_router(
CallsRouter(
base_url=base_url,
templater=self.templater,
config_manager=self.config_manager,
logger=self.logger,
).get_router()
)
self.router.include_router(
TwiMLRouter(
base_url=base_url, templater=self.templater, logger=self.logger
).get_router()
)
for config in inbound_call_configs:
self.router.add_api_route(
config.url,
self.create_inbound_route(
agent_config=config.agent_config,
twilio_config=config.twilio_config,
transcriber_config=config.transcriber_config,
synthesizer_config=config.synthesizer_config,
),
methods=["POST"],
)
logger.info(f"Set up inbound call TwiML at https://{base_url}{config.url}")
def create_inbound_route(
self,
agent_config: AgentConfig,
twilio_config: TwilioConfig,
transcriber_config: Optional[TranscriberConfig] = None,
synthesizer_config: Optional[SynthesizerConfig] = None,
):
def route(twilio_sid: str = Form(alias="CallSid")) -> Response:
call_config = CallConfig(
transcriber_config=transcriber_config
or DeepgramTranscriberConfig(
sampling_rate=DEFAULT_SAMPLING_RATE,
audio_encoding=DEFAULT_AUDIO_ENCODING,
chunk_size=DEFAULT_CHUNK_SIZE,
model="voicemail",
endpointing_config=PunctuationEndpointingConfig(),
),
agent_config=agent_config,
synthesizer_config=synthesizer_config
or AzureSynthesizerConfig(
sampling_rate=DEFAULT_SAMPLING_RATE,
audio_encoding=DEFAULT_AUDIO_ENCODING,
),
twilio_config=twilio_config,
twilio_sid=twilio_sid,
)
conversation_id = create_conversation_id()
self.config_manager.save_config(conversation_id, call_config)
return self.templater.get_connection_twiml(
base_url=self.base_url, call_id=conversation_id
)
return route
async def end_outbound_call(self, conversation_id: str):
# TODO validation via twilio_client
call_config = self.config_manager.get_config(conversation_id)
if not call_config:
raise ValueError("Call not found")
call = Call.from_call_config(
self.base_url,
call_config,
self.config_manager,
conversation_id,
self.logger,
)
call.end_twilio_call()
return {"id": call.id}
def get_router(self) -> APIRouter:
return self.router

View file

@ -0,0 +1,45 @@
from typing import Optional
import logging
from fastapi import APIRouter, HTTPException, WebSocket
from vocode.streaming.telephony.config_manager.base_config_manager import (
BaseConfigManager,
)
from vocode.streaming.telephony.conversation.call import Call
from vocode.streaming.telephony.templates import Templater
class CallsRouter:
def __init__(
self,
base_url: str,
templater: Templater,
config_manager: BaseConfigManager,
logger: Optional[logging.Logger] = None,
):
super().__init__()
self.base_url = base_url
self.templater = templater
self.config_manager = config_manager
self.logger = logger or logging.getLogger(__name__)
self.router = APIRouter()
self.router.websocket("/connect_call/{id}")(self.connect_call)
async def connect_call(self, websocket: WebSocket, id: str):
await websocket.accept()
self.logger.debug("Phone WS connection opened for chat {}".format(id))
call_config = self.config_manager.get_config(id)
if not call_config:
raise HTTPException(status_code=400, detail="No active phone call")
call: Call = Call.from_call_config(
self.base_url, call_config, self.config_manager, id, self.logger
)
await call.attach_ws_and_start(websocket)
self.config_manager.delete_config(call.id)
self.logger.debug("Phone WS connection closed for chat {}".format(id))
def get_router(self) -> APIRouter:
return self.router

View file

@ -0,0 +1,29 @@
import logging
from typing import Optional
from fastapi import APIRouter
from vocode.streaming.telephony.templates import Templater
class TwiMLRouter:
def __init__(
self,
base_url: str,
templater: Templater,
logger: Optional[logging.Logger] = None,
):
super().__init__()
self.base_url = base_url
self.templater = templater
self.logger = logger or logging.getLogger(__name__)
self.router = APIRouter()
self.router.add_api_route(
"/twiml/initiate_call/{id}", self.call_twiml, methods=["POST"]
)
def call_twiml(self, id: str):
return self.templater.get_connection_twiml(base_url=self.base_url, call_id=id)
def get_router(self) -> APIRouter:
return self.router

View file

@ -0,0 +1,20 @@
import os
from jinja2 import Environment, FileSystemLoader
from fastapi import Response
class Templater:
def __init__(self):
self.templates = Environment(
loader=FileSystemLoader("%s/templates/" % os.path.dirname(__file__))
)
def render_template(self, template_name: str, **kwargs):
template = self.templates.get_template(template_name)
return template.render(**kwargs)
def get_connection_twiml(self, call_id: str, base_url: str):
return Response(
self.render_template("connect_call.xml", base_url=base_url, id=call_id),
media_type="application/xml",
)

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://{{ base_url }}/connect_call/{{ id }}" />
</Connect>
</Response>

View file

@ -0,0 +1,12 @@
import os
from typing import Optional
from dotenv import load_dotenv
from twilio.rest import Client
from vocode.streaming.models.telephony import TwilioConfig
load_dotenv()
def create_twilio_client(twilio_config: TwilioConfig):
return Client(twilio_config.account_sid, twilio_config.auth_token)