diff --git a/examples/server/README.md b/examples/server/README.md index 7aa498649..defd17943 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -1,13 +1,34 @@ # Server Example -This is an example server based on [Santa Cat](https://santacat.ai). You can run the server with this command: +Use this server app to quickly host a bot on the web: ``` flask --app daily-bot-manager.py --debug run ``` -Once the server is started, you can load `http://127.0.0.1:5000/spin-up-kitty` in a browser, and the server will do the following: +It's currently configured to serve example apps defined in the APPS constant in the server file: + +``` +chatbot +patient-intake +storybot +translator +``` + +Once the server is started, you can create a bot instance by opening `http://127.0.0.1:5000/start/chatbot` in a browser, and the server will do the following: - Create a new, randomly-named Daily room with `DAILY_API_KEY` from your .env file or environment -- Start the `10-wake-word.py` example and connect it to that room +- Start an instance of `chatbot.py` and connect it to that room - 301 redirect your browser to the room + +### Options + +The server supports several options, which can be set in the body of a POST request, or as params in the URL of a GET request. + +- `room_url` (default: none): A room URL to join. If empty, the server will create a Daily room and return the URL in the response. + room_properties (none): A JSON object (URL encoded if included as a GET parameter) for overriding default room creation properties, as described here: https://docs.daily.co/reference/rest-api/rooms/create-room This will be ignored if a room_url is provided. +- `token_properties` (none): A JSON object (URL encoded if included as a GET parameter) for overriding default token properties. By default, the server creates an owner token with an expiration time of one hour. +- `duration` (7200 seconds, or two hours): Use this property to set a time limit for the bot, as well as an expiration time for the room (if the server is creating one). This will not add an expiration time to an existing room. Expiration times in `token_properties` or `room_properties` will also take precedence over this value. You can set this property to `0` to disable timeouts, but this isn't recommended. +- `bot_args` (none): A string containing any additional command-line args to pass to the bot. +- `wait_for_bot` (true): Whether to wait for the bot to successfully join the room before returning a response from the server. If true, the server will start the bot script, then poll the room for up to 5 seconds to confirm the bot has joined the room. If it doesn't, the server will stop the bot and return a 500 response. If set to `false`, the server will start the bot, but immediately return a 200 response. This can be useful if the server is creating rooms for you, and you need the room URL to join the user to the room. +- `redirect` (true): Instead of returning a 200 for GET requests, the server will return a 301 redirect to the ROOM_URL. This is handy for testing by creating a bot with a GET request directly in the browser. POST requests will never return redirects. Set to `false` to get 200 responses with info in a JSON object even for GET requests. diff --git a/examples/server/auth.py b/examples/server/auth.py deleted file mode 100644 index 7b1f812ec..000000000 --- a/examples/server/auth.py +++ /dev/null @@ -1,31 +0,0 @@ -import time -import urllib - -import requests -from flask import jsonify -import os - - -def get_meeting_token(room_name, daily_api_key, token_expiry): - api_path = os.getenv('DAILY_API_PATH') or 'https://api.daily.co/v1' - - if not token_expiry: - token_expiry = time.time() + 600 - res = requests.post( - f'{api_path}/meeting-tokens', - headers={ - 'Authorization': f'Bearer {daily_api_key}'}, - json={ - 'properties': { - 'room_name': room_name, - 'is_owner': True, - 'exp': token_expiry}}) - if res.status_code != 200: - return jsonify( - {'error': 'Unable to create meeting token', 'detail': res.text}), 500 - meeting_token = res.json()['token'] - return meeting_token - - -def get_room_name(room_url): - return urllib.parse.urlparse(room_url).path[1:] diff --git a/examples/server/daily-bot-manager.py b/examples/server/daily-bot-manager.py index a1c2c102c..8d9c1ca02 100644 --- a/examples/server/daily-bot-manager.py +++ b/examples/server/daily-bot-manager.py @@ -1,101 +1,163 @@ import os import requests +import urllib import subprocess import time -from flask import Flask, jsonify, redirect +from flask import Flask, jsonify, redirect, request from flask_cors import CORS -from auth import get_meeting_token - from dotenv import load_dotenv load_dotenv(override=True) app = Flask(__name__) CORS(app) -print( - f"I loaded an environment, and my FAL_KEY_ID is {os.getenv('FAL_KEY_ID')}") +APPS = { + "chatbot": "examples/starter-apps/chatbot.py", + "patient-intake": "examples/starter-apps/patient-intake.py", + "storybot": "examples/starter-apps/storybot.py", + "translator": "examples/starter-apps/translator.py" +} + +daily_api_key = os.getenv("DAILY_API_KEY") +api_path = os.getenv("DAILY_API_PATH") or "https://api.daily.co/v1" + +def get_room_name(room_url): + return urllib.parse.urlparse(room_url).path[1:] -def start_bot(bot_path, args=None): - daily_api_key = os.getenv("DAILY_API_KEY") - api_path = os.getenv("DAILY_API_PATH") or "https://api.daily.co/v1" - timeout = int(os.getenv("DAILY_ROOM_TIMEOUT") - or os.getenv("DAILY_BOT_MAX_DURATION") or 300) - exp = time.time() + timeout +def create_room(room_properties, exp): + room_props = { + "exp": exp, + "enable_chat": True, + "enable_emoji_reactions": True, + "eject_at_room_exp": True, + "enable_prejoin_ui": False, + "enable_recording": "cloud" + } + if room_properties: + room_props |= room_properties + res = requests.post( f"{api_path}/rooms", headers={"Authorization": f"Bearer {daily_api_key}"}, json={ - "properties": { - "exp": exp, - "enable_chat": True, - "enable_emoji_reactions": True, - "eject_at_room_exp": True, - "enable_prejoin_ui": False, - "enable_recording": "cloud" - } + "properties": room_props }, ) if res.status_code != 200: - return ( - jsonify( - { - "error": "Unable to create room", - "status_code": res.status_code, - "text": res.text, - } - ), - 500, - ) + raise Exception(f"Unable to create room: {res.text}") + room_url = res.json()["url"] room_name = res.json()["name"] + return (room_url, room_name) + + +def create_token(room_name, token_properties, exp): + token_props = {"exp": exp, "is_owner": True} + if token_properties: + token_props |= token_properties + # Force the token to be limited to the room + token_props |= {"room_name": room_name} + res = requests.post( + f'{api_path}/meeting-tokens', + headers={ + 'Authorization': f'Bearer {daily_api_key}'}, + json={ + 'properties': token_props}) + if res.status_code != 200: + if res.status_code != 200: + raise Exception(f"Unable to create meeting token: {res.text}") + + meeting_token = res.json()['token'] + return meeting_token - meeting_token = get_meeting_token(room_name, daily_api_key, exp) - if args: - extra_args = " ".join([f'-{x[0]} "{x[1]}"' for x in args]) - else: - extra_args = "" +def start_bot(*, bot_path, room_url, token, bot_args, wait_for_bot): + room_name = get_room_name(room_url) proc = subprocess.Popen( - [f"python {bot_path} -u {room_url} -t {meeting_token} -k {daily_api_key} {extra_args}"], + [f"python {bot_path} -u {room_url} -t {token} -k {daily_api_key} {bot_args}"], shell=True, bufsize=1, ) - # Don't return until the bot has joined the room, but wait for at most 2 - # seconds. - attempts = 0 - while attempts < 20: - time.sleep(0.1) - attempts += 1 - res = requests.get( - f"{api_path}/rooms/{room_name}/get-session-data", - headers={"Authorization": f"Bearer {daily_api_key}"}, - ) - if res.status_code == 200: - break - print(f"Took {attempts} attempts to join room {room_name}") - - # Additional client config - config = {} - if os.getenv("CLIENT_VAD_TIMEOUT_SEC"): - config['vad_timeout_sec'] = float( - os.getenv("DAILY_CLIENT_VAD_TIMEOUT_SEC")) - else: - config['vad_timeout_sec'] = 1.5 - - # return jsonify({"room_url": room_url, "token": meeting_token, "config": - # config}), 200 - return redirect(room_url, code=301) - - -@app.route("/spin-up-kitty", methods=["GET", "POST"]) -def spin_up_kitty(): - return start_bot("./examples/foundational/10-wake-word.py") + if wait_for_bot: + # Don't return until the bot has joined the room, but wait for at most 5 + # seconds. + attempts = 0 + while attempts < 50: + time.sleep(0.1) + attempts += 1 + res = requests.get( + f"{api_path}/rooms/{room_name}/get-session-data", + headers={"Authorization": f"Bearer {daily_api_key}"}, + ) + if res.status_code == 200: + print(f"Took {attempts} attempts to join room {room_name}") + return True + + # If we don't break from the loop, that means we never found the bot in the room + raise Exception("The bot was unable to join the room. Please try again.") + + return True + + +@app.route("/start/", methods=["GET", "POST"]) +def start(botname): + try: + if botname not in APPS: + raise Exception(f"Bot '{botname}' is not in the allowlist.") + + bot_path = APPS[botname] + props = { + "room_url": None, + "room_properties": None, + "token_properties": None, + "bot_args": None, + "wait_for_bot": True, + "duration": None, + "redirect": True + } + props |= request.values.to_dict() # gets URL params as well as plaintext POST body + try: + props |= request.json + except BaseException: + pass + if props['redirect'] == "false": + props['redirect'] = False + if props['wait_for_bot'] == "false": + props['wait_for_bot'] = False + + duration = int(os.getenv("DAILY_BOT_DURATION") or 7200) + if props['duration']: + duration = props['duration'] + exp = time.time() + duration + if (props['room_url']): + room_url = props['room_url'] + try: + room_name = get_room_name(room_url) + except ValueError: + raise Exception( + "There was a problem detecting the room name. Please double-check the value of room_url.") + else: + room_url, room_name = create_room(props['room_properties'], exp) + token = create_token(room_name, props['token_properties'], exp) + bot = start_bot( + room_url=room_url, + bot_path=bot_path, + token=token, + bot_args=props['bot_args'], + wait_for_bot=props['wait_for_bot']) + + if props['redirect'] and request.method == "GET": + return redirect(room_url, 302) + else: + return jsonify({"room_url": room_url, "token": token}) + except BaseException as e: + return "There was a problem starting the bot: {e}", 500 @app.route("/healthz")