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 = () => {
>
-