From dab01e0d58769712f28d096cf41e4da2adeedc60 Mon Sep 17 00:00:00 2001 From: Jon Taylor Date: Thu, 16 May 2024 18:57:34 +0100 Subject: [PATCH] fixed runner --- examples/client/env.example | 2 - examples/simple-chatbot/bot.py | 14 +- examples/simple-chatbot/bot_runner.py | 166 ++++++++++++++++++ .../{utils => }/daily_helpers.py | 28 ++- examples/simple-chatbot/pipecat.py | 89 ---------- examples/simple-chatbot/requirements.txt | 3 +- examples/{client => web-ui}/.eslintrc.cjs | 0 examples/{client => web-ui}/.gitignore | 0 examples/{client => web-ui}/README.md | 0 examples/web-ui/env.example | 1 + examples/{client => web-ui}/index.html | 0 examples/{client => web-ui}/package.json | 0 .../{client => web-ui}/public/favicon.ico | Bin examples/{client => web-ui}/src/App.tsx | 16 +- .../{client => web-ui}/src/assets/logo.svg | 0 .../src/components/AudioIndicator/index.tsx | 0 .../AudioIndicator/styles.module.css | 0 .../src/components/DeviceSelect/index.tsx | 0 .../components/DeviceSelect/styles.module.css | 0 .../src/components/Session/agent.tsx | 0 .../src/components/Session/index.tsx | 4 +- .../src/components/Session/status.tsx | 0 .../src/components/Session/styles.module.css | 0 .../src/components/UserMicBubble/index.tsx | 0 .../UserMicBubble/styles.module.css | 0 .../src/components/alert.tsx | 0 .../src/components/button.tsx | 0 .../src/components/header.tsx | 0 .../src/components/logo.tsx | 0 examples/{client => web-ui}/src/global.css | 0 examples/{client => web-ui}/src/main.tsx | 0 .../{client => web-ui}/src/utils/daily.js | 0 examples/{client => web-ui}/src/vite-env.d.ts | 0 examples/{client => web-ui}/tsconfig.json | 0 .../{client => web-ui}/tsconfig.node.json | 0 examples/{client => web-ui}/vite.config.ts | 0 examples/{client => web-ui}/yarn.lock | 0 37 files changed, 214 insertions(+), 109 deletions(-) delete mode 100644 examples/client/env.example create mode 100644 examples/simple-chatbot/bot_runner.py rename examples/simple-chatbot/{utils => }/daily_helpers.py (81%) delete mode 100644 examples/simple-chatbot/pipecat.py rename examples/{client => web-ui}/.eslintrc.cjs (100%) rename examples/{client => web-ui}/.gitignore (100%) rename examples/{client => web-ui}/README.md (100%) create mode 100644 examples/web-ui/env.example rename examples/{client => web-ui}/index.html (100%) rename examples/{client => web-ui}/package.json (100%) rename examples/{client => web-ui}/public/favicon.ico (100%) rename examples/{client => web-ui}/src/App.tsx (92%) rename examples/{client => web-ui}/src/assets/logo.svg (100%) rename examples/{client => web-ui}/src/components/AudioIndicator/index.tsx (100%) rename examples/{client => web-ui}/src/components/AudioIndicator/styles.module.css (100%) rename examples/{client => web-ui}/src/components/DeviceSelect/index.tsx (100%) rename examples/{client => web-ui}/src/components/DeviceSelect/styles.module.css (100%) rename examples/{client => web-ui}/src/components/Session/agent.tsx (100%) rename examples/{client => web-ui}/src/components/Session/index.tsx (94%) rename examples/{client => web-ui}/src/components/Session/status.tsx (100%) rename examples/{client => web-ui}/src/components/Session/styles.module.css (100%) rename examples/{client => web-ui}/src/components/UserMicBubble/index.tsx (100%) rename examples/{client => web-ui}/src/components/UserMicBubble/styles.module.css (100%) rename examples/{client => web-ui}/src/components/alert.tsx (100%) rename examples/{client => web-ui}/src/components/button.tsx (100%) rename examples/{client => web-ui}/src/components/header.tsx (100%) rename examples/{client => web-ui}/src/components/logo.tsx (100%) rename examples/{client => web-ui}/src/global.css (100%) rename examples/{client => web-ui}/src/main.tsx (100%) rename examples/{client => web-ui}/src/utils/daily.js (100%) rename examples/{client => web-ui}/src/vite-env.d.ts (100%) rename examples/{client => web-ui}/tsconfig.json (100%) rename examples/{client => web-ui}/tsconfig.node.json (100%) rename examples/{client => web-ui}/vite.config.ts (100%) rename examples/{client => web-ui}/yarn.lock (100%) diff --git a/examples/client/env.example b/examples/client/env.example deleted file mode 100644 index a96939628..000000000 --- a/examples/client/env.example +++ /dev/null @@ -1,2 +0,0 @@ -VITE_SERVER_URL... #optional: if serving frontend independetely from backend (otherwise relative) -VITE_TRANSPORT_ROOM_URL=... #optional: use the same room each time (vs. creating a new one) diff --git a/examples/simple-chatbot/bot.py b/examples/simple-chatbot/bot.py index e7be4732d..f88a81a60 100644 --- a/examples/simple-chatbot/bot.py +++ b/examples/simple-chatbot/bot.py @@ -2,6 +2,7 @@ import asyncio import aiohttp import os import sys +import argparse from PIL import Image @@ -23,8 +24,6 @@ from pipecat.services.openai import OpenAILLMService from pipecat.transports.services.daily import DailyParams, DailyTranscriptionSettings, DailyTransport from pipecat.vad.silero import SileroVAD -from runner import configure - from loguru import logger from dotenv import load_dotenv @@ -43,7 +42,8 @@ for i in range(1, 26): # Get the filename without the extension to use as the dictionary key # Open the image and convert it to bytes with Image.open(full_path) as img: - sprites.append(ImageRawFrame(image=img.tobytes(), size=img.size, format=img.format)) + sprites.append(ImageRawFrame(image=img.tobytes(), + size=img.size, format=img.format)) flipped = sprites[::-1] sprites.extend(flipped) @@ -156,5 +156,9 @@ async def main(room_url: str, token): if __name__ == "__main__": - (url, token) = configure() - asyncio.run(main(url, token)) + parser = argparse.ArgumentParser(description="Daily Storyteller Bot") + parser.add_argument("-u", type=str, help="Room URL") + parser.add_argument("-t", type=str, help="Token") + config = parser.parse_args() + + asyncio.run(main(config.u, config.t)) diff --git a/examples/simple-chatbot/bot_runner.py b/examples/simple-chatbot/bot_runner.py new file mode 100644 index 000000000..209f18b11 --- /dev/null +++ b/examples/simple-chatbot/bot_runner.py @@ -0,0 +1,166 @@ +from daily_helpers import create_room, get_token, check_room_url +import os +import sys +import argparse +import subprocess +import atexit +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, JSONResponse + +from dotenv import load_dotenv +load_dotenv(override=True) + +# Bot sub-process dict for status reporting and concurrency control +bot_procs = {} + + +def cleanup(): + # Clean up function, just to be extra safe + for proc in bot_procs.values(): + proc[0].terminate() + proc[0].wait() + + +atexit.register(cleanup) + +# ------------ Configuration ------------ # + +MAX_SESSION_TIME = 5 * 1000 +BOT_CAN_IDLE = True +SERVE_STATIC = True +STATIC_DIR = "../web-ui/dist" +STATIC_ROUTE = "/static" +STATIC_INDEX = "index.html" + + +# ----------------- API ----------------- # + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"] +) + +# Optionally serve client static files +if SERVE_STATIC: + app.mount(STATIC_ROUTE, StaticFiles( + directory=STATIC_DIR, html=True), name="static") + + @app.get("/{path_name:path}", response_class=FileResponse) + async def catch_all(path_name: Optional[str] = ""): + if path_name == "": + return FileResponse(f"{STATIC_DIR}/{STATIC_INDEX}") + + file_path = Path(STATIC_DIR) / (path_name or "") + + if file_path.is_file(): + return file_path + + html_file_path = file_path.with_suffix(".html") + if html_file_path.is_file(): + return FileResponse(html_file_path) + + raise HTTPException( + status_code=404, detail="Page not found") + + +@app.post("/start_bot") +async def start_bot(request: Request) -> JSONResponse: + try: + data = await request.json() + # Is this a webhook creation request? + if "test" in data: + return JSONResponse({"test": True}) + except Exception: + pass + + # Use specified room URL, or create a new one if not specified + room_url = os.getenv("DAILY_SAMPLE_ROOM_URL", None) + + if not room_url: + try: + room_url, room_name = create_room() + except Exception: + raise HTTPException( + status_code=500, + detail="Unable to provision room") + else: + # Check passed room URL exists + try: + check_room_url(room_url) + except Exception: + raise HTTPException( + status_code=500, detail=f"Room not found: {room_url}") + + # Give the agent a token to join the session + token = get_token(room_url) + + if not room_url or not token: + raise HTTPException( + status_code=500, detail=f"Failed to get token for room: {room_url}") + + # Spawn a new agent, and join the user session + # Note: this is mostly for demonstration purposes (refer to 'deployment' in README) + + # @TODO: Spawn a new fly machine here... + try: + proc = subprocess.Popen( + [ + f"python3 -m bot -u {room_url} -t {token}" + ], + shell=True, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)) + ) + bot_procs[proc.pid] = (proc, room_url) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to start subprocess: {e}") + + # Grab a token for the user to join with + user_token = get_token(room_url) + + return JSONResponse({"bot_id": proc.pid, "room_url": room_url, "token": user_token}) + + +# ----------------- Main ----------------- # + +if __name__ == "__main__": + # Check environment variables + required_env_vars = ['OPENAI_API_KEY', 'DAILY_API_KEY', + 'ELEVENLABS_VOICE_ID', 'ELEVENLABS_API_KEY'] + for env_var in required_env_vars: + if env_var not in os.environ: + raise Exception(f"Missing environment variable: {env_var}.") + + parser = argparse.ArgumentParser(description="Pipecat Bot Runner") + parser.add_argument("--host", type=str, + default=os.getenv("HOST", "localhost"), help="Host address") + parser.add_argument("--port", type=int, + default=os.getenv("PORT", 7860), help="Port number") + parser.add_argument("--reload", action="store_true", + default=True, help="Reload code on change") + + config = parser.parse_args() + + try: + import uvicorn + + uvicorn.run( + "bot_runner:app", + host=config.host, + port=config.port, + reload=config.reload + ) + + except KeyboardInterrupt: + print("Pipecat runner shutting down...") diff --git a/examples/simple-chatbot/utils/daily_helpers.py b/examples/simple-chatbot/daily_helpers.py similarity index 81% rename from examples/simple-chatbot/utils/daily_helpers.py rename to examples/simple-chatbot/daily_helpers.py index 140f710e4..d7c9da8c6 100644 --- a/examples/simple-chatbot/utils/daily_helpers.py +++ b/examples/simple-chatbot/daily_helpers.py @@ -1,4 +1,5 @@ +from re import X import urllib.parse import os import time @@ -9,7 +10,7 @@ from dotenv import load_dotenv load_dotenv() -daily_api_path = os.getenv("DAILY_API_URL") or "api.daily.co/v1" +daily_api_path = os.getenv("DAILY_API_URL", "api.daily.co/v1") daily_api_key = os.getenv("DAILY_API_KEY") @@ -50,6 +51,31 @@ def create_room() -> tuple[str, str]: return room_url, room_name +def check_room_url(room_url: str) -> bool: + """ + Checks if a room exists in Daily. + # See: https://docs.daily.co/reference/rest-api/rooms/get-room-config + + Args: + room_name (str): The url of the room to check for + + Returns: + bool: True if 200 OK, Exception otherwise. + """ + + room_name = get_name_from_url(room_url) + + res: requests.Response = requests.get( + f"https://{daily_api_path}/rooms/{room_name}", + headers={"Authorization": f"Bearer {daily_api_key}"} + ) + + if res.status_code != 200: + raise Exception(f"Room not found: {room_name}") + + return True + + def get_name_from_url(room_url: str) -> str: """ Extracts the name from a given room URL. diff --git a/examples/simple-chatbot/pipecat.py b/examples/simple-chatbot/pipecat.py deleted file mode 100644 index 7c0576923..000000000 --- a/examples/simple-chatbot/pipecat.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import argparse -import uvicorn - -from typing import Optional -from pathlib import Path - -from fastapi import FastAPI, Request, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles -from fastapi.responses import FileResponse, JSONResponse - -from dotenv import load_dotenv -load_dotenv(override=True) - - -# ------------ Configuration ------------ # - -MAX_SESSION_TIME = 5 * 1000 -BOT_CAN_IDLE = True -SERVE_STATIC = True -STATIC_DIR = "client/dist" -STATIC_ROUTE = "/static" -STATIC_INDEX = "index.html" - - -# ----------------- API ----------------- # - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"] -) - -# Optionally serve client static files -if SERVE_STATIC: - app.mount(STATIC_ROUTE, StaticFiles( - directory=STATIC_DIR, html=True), name="static") - - @app.get("/{path_name:path}", response_class=FileResponse) - async def catch_all(path_name: Optional[str] = ""): - if path_name == "": - return FileResponse(f"{STATIC_DIR}/{STATIC_INDEX}") - - file_path = Path(STATIC_DIR) / (path_name or "") - - if file_path.is_file(): - return file_path - - html_file_path = file_path.with_suffix(".html") - if html_file_path.is_file(): - return FileResponse(html_file_path) - - raise HTTPException( - status_code=404, detail="Page not found") - - -@app.post("/start_bot") -async def start_bot(request: Request): - return JSONResponse({"bot_id": 123, "user_token": "abc", "room_url": "https://jpt.daily.co/hello"}) - - -# ----------------- Main ----------------- # - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Pipecat Bot Runner") - parser.add_argument("--host", type=str, - default=os.getenv("HOST", "localhost"), help="Host address") - parser.add_argument("--port", type=int, - default=os.getenv("PORT", 7860), help="Port number") - parser.add_argument("--reload", action="store_true", - default=True, help="Reload code on change") - - config = parser.parse_args() - - try: - uvicorn.run( - "pipecat:app", - host=config.host, - port=config.port, - reload=config.reload - ) - - except KeyboardInterrupt: - print("Pipecat runner shutting down...") diff --git a/examples/simple-chatbot/requirements.txt b/examples/simple-chatbot/requirements.txt index 0b2b8f7b2..f6b609632 100644 --- a/examples/simple-chatbot/requirements.txt +++ b/examples/simple-chatbot/requirements.txt @@ -3,4 +3,5 @@ fastapi uvicorn requests python-dotenv -loguru \ No newline at end of file +loguru +requests \ No newline at end of file diff --git a/examples/client/.eslintrc.cjs b/examples/web-ui/.eslintrc.cjs similarity index 100% rename from examples/client/.eslintrc.cjs rename to examples/web-ui/.eslintrc.cjs diff --git a/examples/client/.gitignore b/examples/web-ui/.gitignore similarity index 100% rename from examples/client/.gitignore rename to examples/web-ui/.gitignore diff --git a/examples/client/README.md b/examples/web-ui/README.md similarity index 100% rename from examples/client/README.md rename to examples/web-ui/README.md diff --git a/examples/web-ui/env.example b/examples/web-ui/env.example new file mode 100644 index 000000000..379185947 --- /dev/null +++ b/examples/web-ui/env.example @@ -0,0 +1 @@ +VITE_SERVER_URL=... #optional: if serving frontend independetely from backend (otherwise relative.) \ No newline at end of file diff --git a/examples/client/index.html b/examples/web-ui/index.html similarity index 100% rename from examples/client/index.html rename to examples/web-ui/index.html diff --git a/examples/client/package.json b/examples/web-ui/package.json similarity index 100% rename from examples/client/package.json rename to examples/web-ui/package.json diff --git a/examples/client/public/favicon.ico b/examples/web-ui/public/favicon.ico similarity index 100% rename from examples/client/public/favicon.ico rename to examples/web-ui/public/favicon.ico diff --git a/examples/client/src/App.tsx b/examples/web-ui/src/App.tsx similarity index 92% rename from examples/client/src/App.tsx rename to examples/web-ui/src/App.tsx index 253bc9e1c..46a18640c 100644 --- a/examples/client/src/App.tsx +++ b/examples/web-ui/src/App.tsx @@ -37,14 +37,11 @@ export default function App() { let data; try { - const res = await fetch(`${serverUrl}/start_bot`, { + const res = await fetch(`${serverUrl}start_bot`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ - room_url: import.meta.env.VITE_TRANSPORT_ROOM_URL || null, - }), }); data = await res.json(); @@ -52,6 +49,7 @@ export default function App() { if (!res.ok) { setError(data.detail); setState("error"); + return; } } catch (e) { setError( @@ -65,7 +63,7 @@ export default function App() { await daily.join({ url: data.room_url, - token: data.user_token, + token: data.token, videoSource: false, startAudioOff: true, }); @@ -73,10 +71,10 @@ export default function App() { setState("connected"); } - /*async function leave() { + async function leave() { await daily?.leave(); - setState("finished"); - }*/ + setState("idle"); + } if (state === "error") { return ( @@ -87,7 +85,7 @@ export default function App() { } if (state === "connected") { - return ; + return leave()} />; } const status_text = { diff --git a/examples/client/src/assets/logo.svg b/examples/web-ui/src/assets/logo.svg similarity index 100% rename from examples/client/src/assets/logo.svg rename to examples/web-ui/src/assets/logo.svg diff --git a/examples/client/src/components/AudioIndicator/index.tsx b/examples/web-ui/src/components/AudioIndicator/index.tsx similarity index 100% rename from examples/client/src/components/AudioIndicator/index.tsx rename to examples/web-ui/src/components/AudioIndicator/index.tsx diff --git a/examples/client/src/components/AudioIndicator/styles.module.css b/examples/web-ui/src/components/AudioIndicator/styles.module.css similarity index 100% rename from examples/client/src/components/AudioIndicator/styles.module.css rename to examples/web-ui/src/components/AudioIndicator/styles.module.css diff --git a/examples/client/src/components/DeviceSelect/index.tsx b/examples/web-ui/src/components/DeviceSelect/index.tsx similarity index 100% rename from examples/client/src/components/DeviceSelect/index.tsx rename to examples/web-ui/src/components/DeviceSelect/index.tsx diff --git a/examples/client/src/components/DeviceSelect/styles.module.css b/examples/web-ui/src/components/DeviceSelect/styles.module.css similarity index 100% rename from examples/client/src/components/DeviceSelect/styles.module.css rename to examples/web-ui/src/components/DeviceSelect/styles.module.css diff --git a/examples/client/src/components/Session/agent.tsx b/examples/web-ui/src/components/Session/agent.tsx similarity index 100% rename from examples/client/src/components/Session/agent.tsx rename to examples/web-ui/src/components/Session/agent.tsx diff --git a/examples/client/src/components/Session/index.tsx b/examples/web-ui/src/components/Session/index.tsx similarity index 94% rename from examples/client/src/components/Session/index.tsx rename to examples/web-ui/src/components/Session/index.tsx index 797a9050b..609ef8b44 100644 --- a/examples/client/src/components/Session/index.tsx +++ b/examples/web-ui/src/components/Session/index.tsx @@ -9,7 +9,7 @@ import UserMicBubble from "../UserMicBubble"; import styles from "./styles.module.css"; -export const Session: React.FC = () => { +export const Session: React.FC<{ onLeave: () => void }> = ({ onLeave }) => { const daily = useDaily(); const [showDevices, setShowDevices] = useState(false); const modalRef = useRef(null); @@ -65,7 +65,7 @@ export const Session: React.FC = () => { > - diff --git a/examples/client/src/components/Session/status.tsx b/examples/web-ui/src/components/Session/status.tsx similarity index 100% rename from examples/client/src/components/Session/status.tsx rename to examples/web-ui/src/components/Session/status.tsx diff --git a/examples/client/src/components/Session/styles.module.css b/examples/web-ui/src/components/Session/styles.module.css similarity index 100% rename from examples/client/src/components/Session/styles.module.css rename to examples/web-ui/src/components/Session/styles.module.css diff --git a/examples/client/src/components/UserMicBubble/index.tsx b/examples/web-ui/src/components/UserMicBubble/index.tsx similarity index 100% rename from examples/client/src/components/UserMicBubble/index.tsx rename to examples/web-ui/src/components/UserMicBubble/index.tsx diff --git a/examples/client/src/components/UserMicBubble/styles.module.css b/examples/web-ui/src/components/UserMicBubble/styles.module.css similarity index 100% rename from examples/client/src/components/UserMicBubble/styles.module.css rename to examples/web-ui/src/components/UserMicBubble/styles.module.css diff --git a/examples/client/src/components/alert.tsx b/examples/web-ui/src/components/alert.tsx similarity index 100% rename from examples/client/src/components/alert.tsx rename to examples/web-ui/src/components/alert.tsx diff --git a/examples/client/src/components/button.tsx b/examples/web-ui/src/components/button.tsx similarity index 100% rename from examples/client/src/components/button.tsx rename to examples/web-ui/src/components/button.tsx diff --git a/examples/client/src/components/header.tsx b/examples/web-ui/src/components/header.tsx similarity index 100% rename from examples/client/src/components/header.tsx rename to examples/web-ui/src/components/header.tsx diff --git a/examples/client/src/components/logo.tsx b/examples/web-ui/src/components/logo.tsx similarity index 100% rename from examples/client/src/components/logo.tsx rename to examples/web-ui/src/components/logo.tsx diff --git a/examples/client/src/global.css b/examples/web-ui/src/global.css similarity index 100% rename from examples/client/src/global.css rename to examples/web-ui/src/global.css diff --git a/examples/client/src/main.tsx b/examples/web-ui/src/main.tsx similarity index 100% rename from examples/client/src/main.tsx rename to examples/web-ui/src/main.tsx diff --git a/examples/client/src/utils/daily.js b/examples/web-ui/src/utils/daily.js similarity index 100% rename from examples/client/src/utils/daily.js rename to examples/web-ui/src/utils/daily.js diff --git a/examples/client/src/vite-env.d.ts b/examples/web-ui/src/vite-env.d.ts similarity index 100% rename from examples/client/src/vite-env.d.ts rename to examples/web-ui/src/vite-env.d.ts diff --git a/examples/client/tsconfig.json b/examples/web-ui/tsconfig.json similarity index 100% rename from examples/client/tsconfig.json rename to examples/web-ui/tsconfig.json diff --git a/examples/client/tsconfig.node.json b/examples/web-ui/tsconfig.node.json similarity index 100% rename from examples/client/tsconfig.node.json rename to examples/web-ui/tsconfig.node.json diff --git a/examples/client/vite.config.ts b/examples/web-ui/vite.config.ts similarity index 100% rename from examples/client/vite.config.ts rename to examples/web-ui/vite.config.ts diff --git a/examples/client/yarn.lock b/examples/web-ui/yarn.lock similarity index 100% rename from examples/client/yarn.lock rename to examples/web-ui/yarn.lock