diff --git a/examples/client/src/components/Session/index.tsx b/examples/client/src/components/Session/index.tsx index 66b6077a9..797a9050b 100644 --- a/examples/client/src/components/Session/index.tsx +++ b/examples/client/src/components/Session/index.tsx @@ -1,17 +1,35 @@ import React, { useEffect, useRef, useState } from "react"; import { LogOut, Settings } from "lucide-react"; -import { DailyAudio } from "@daily-co/daily-react"; +import { DailyAudio, useAppMessage, useDaily } from "@daily-co/daily-react"; import DeviceSelect from "../DeviceSelect"; import Agent from "./agent"; import { Button } from "../button"; -import styles from "./styles.module.css"; import UserMicBubble from "../UserMicBubble"; +import styles from "./styles.module.css"; + export const Session: React.FC = () => { + const daily = useDaily(); const [showDevices, setShowDevices] = useState(false); - const [canTalk, setCanTalk] = useState(false); const modalRef = useRef(null); + const [talkState, setTalkState] = useState<"user" | "assistant">("assistant"); + + useAppMessage({ + onAppMessage: (e) => { + if (!daily || !e.data?.cue) return; + + // Determine the UI state from the cue sent by the bot + if (e.data?.cue === "user_turn") { + // Delay enabling local mic input to avoid feedback from LLM + setTimeout(() => daily.setLocalAudio(true), 500); + setTalkState("user"); + } else { + daily.setLocalAudio(false); + setTalkState("assistant"); + } + }, + }); useEffect(() => { const current = modalRef.current; @@ -34,7 +52,7 @@ export const Session: React.FC = () => {
- +
diff --git a/examples/client/src/components/UserMicBubble/index.tsx b/examples/client/src/components/UserMicBubble/index.tsx index bede863b7..0891092cb 100644 --- a/examples/client/src/components/UserMicBubble/index.tsx +++ b/examples/client/src/components/UserMicBubble/index.tsx @@ -1,12 +1,12 @@ -import React, { useCallback, useRef, useState, useEffect } from "react"; +import React, { useCallback, useRef } from "react"; import { useAudioLevel, useAudioTrack, useLocalSessionId, - useAppMessage, + //useAppMessage, } from "@daily-co/daily-react"; -import { DailyEventObjectAppMessage } from "@daily-co/daily-js"; +//import { DailyEventObjectAppMessage } from "@daily-co/daily-js"; import { Mic, MicOff } from "lucide-react"; //import { TypewriterEffect } from "../ui/typewriter"; import styles from "./styles.module.css"; @@ -38,13 +38,13 @@ interface Props { } export default function UserMicBubble({ active }: Props) { + /* const [transcription, setTranscription] = useState([]); - useAppMessage({ onAppMessage: (e: DailyEventObjectAppMessage) => { if (e.fromId && e.fromId === "transcription") { if (e.data.user_id === "" && e.data.is_final) { - setTranscription((t) => [...t, ...e.data.text.split(" ")]); + //setTranscription((t) => [...t, ...e.data.text.split(" ")]); } } }, @@ -54,7 +54,7 @@ export default function UserMicBubble({ active }: Props) { if (active) return; const t = setTimeout(() => setTranscription([]), 4000); return () => clearTimeout(t); - }, [active]); + }, [active]);*/ return (
diff --git a/examples/simple-chatbot/pipecat.py b/examples/simple-chatbot/pipecat.py new file mode 100644 index 000000000..7c0576923 --- /dev/null +++ b/examples/simple-chatbot/pipecat.py @@ -0,0 +1,89 @@ +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 11c70f3e1..0b2b8f7b2 100644 --- a/examples/simple-chatbot/requirements.txt +++ b/examples/simple-chatbot/requirements.txt @@ -1,5 +1,6 @@ -python-dotenv -requests -fastapi[all] +pipecat-ai[daily,openai,fal] +fastapi uvicorn -pipecat-ai[daily,openai,silero] +requests +python-dotenv +loguru \ No newline at end of file diff --git a/examples/simple-chatbot/runner.py b/examples/simple-chatbot/runner.py deleted file mode 100644 index 6d1a8113d..000000000 --- a/examples/simple-chatbot/runner.py +++ /dev/null @@ -1,58 +0,0 @@ -import argparse -import os -import time -import urllib -import requests - - -def configure(): - parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample") - parser.add_argument( - "-u", - "--url", - type=str, - required=False, - help="URL of the Daily room to join") - parser.add_argument( - "-k", - "--apikey", - type=str, - required=False, - help="Daily API Key (needed to create an owner token for the room)", - ) - - args, unknown = parser.parse_known_args() - - url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL") - key = args.apikey or os.getenv("DAILY_API_KEY") - - if not url: - raise Exception( - "No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL.") - - if not key: - raise Exception("No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.") - - # Create a meeting token for the given room with an expiration 1 hour in - # the future. - room_name: str = urllib.parse.urlparse(url).path[1:] - expiration: float = time.time() + 60 * 60 - - res: requests.Response = requests.post( - f"https://api.daily.co/v1/meeting-tokens", - headers={ - "Authorization": f"Bearer {key}"}, - json={ - "properties": { - "room_name": room_name, - "is_owner": True, - "exp": expiration}}, - ) - - if res.status_code != 200: - raise Exception( - f"Failed to create meeting token: {res.status_code} {res.text}") - - token: str = res.json()["token"] - - return (url, token) diff --git a/examples/simple-chatbot/server.py b/examples/simple-chatbot/server.py deleted file mode 100644 index dad451407..000000000 --- a/examples/simple-chatbot/server.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -import argparse -import subprocess -import atexit - -from fastapi import FastAPI, Request, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, RedirectResponse - -from utils.daily_helpers import create_room as _create_room, get_token - -MAX_BOTS_PER_ROOM = 1 - -# 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.terminate() - proc.wait() - - -atexit.register(cleanup) - - -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -@app.get("/start") -async def start_agent(request: Request): - print(f"!!! Creating room") - room_url, room_name = _create_room() - print(f"!!! Room URL: {room_url}") - # Ensure the room property is present - if not room_url: - raise HTTPException( - status_code=500, - detail="Missing 'room' property in request data. Cannot start agent without a target room!") - - # Check if there is already an existing process running in this room - num_bots_in_room = sum( - 1 for proc in bot_procs.values() if proc[1] == room_url and proc[0].poll() is None) - if num_bots_in_room >= MAX_BOTS_PER_ROOM: - raise HTTPException( - status_code=500, detail=f"Max bot limited reach for room: {room_url}") - - # Get the token for the room - token = get_token(room_url) - - if 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) - 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}") - - return RedirectResponse(room_url) - - -@app.get("/status/{pid}") -def get_status(pid: int): - # Look up the subprocess - proc = bot_procs.get(pid) - - # If the subprocess doesn't exist, return an error - if not proc: - raise HTTPException( - status_code=404, detail=f"Bot with process id: {pid} not found") - - # Check the status of the subprocess - if proc[0].poll() is None: - status = "running" - else: - status = "finished" - - return JSONResponse({"bot_id": pid, "status": status}) - - -if __name__ == "__main__": - import uvicorn - - default_host = os.getenv("HOST", "0.0.0.0") - default_port = int(os.getenv("FAST_API_PORT", "7860")) - - parser = argparse.ArgumentParser( - description="Daily Storyteller FastAPI server") - parser.add_argument("--host", type=str, - default=default_host, help="Host address") - parser.add_argument("--port", type=int, - default=default_port, help="Port number") - parser.add_argument("--reload", action="store_true", - help="Reload code on change") - - config = parser.parse_args() - - uvicorn.run( - "server:app", - host=config.host, - port=config.port, - reload=config.reload, - )