├── .vscode ├── extensions.json └── settings.json ├── example.env ├── .gitignore ├── api ├── index.ts ├── webhook │ ├── .endOfCallReport.ts │ ├── .hang.ts │ ├── .transcript.ts │ ├── .speechUpdateHandler.ts │ ├── .statusUpdate.ts │ ├── .functionCall.ts │ ├── index.ts │ ├── .assistantRequest.ts │ └── README.md ├── custom-llm │ ├── basic.ts │ ├── openai-sse.ts │ ├── openai-advanced.ts │ └── README.md ├── outbound.ts ├── functions │ ├── rag.ts │ ├── basic.ts │ └── README.md └── inbound.ts ├── functions ├── index.ts ├── fetchKeyword.ts ├── weather.ts ├── getRandomName.ts └── getCharacterInspiration.ts ├── config └── env.config.ts ├── utils └── cors.utils.ts ├── tsconfig.json ├── package.json ├── LICENSE ├── vercel.json ├── assistants ├── basic.json └── custom-llm.json ├── README.md ├── types └── vapi.types.ts └── data ├── Characters.md └── Activities.md /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["denoland.vscode-deno"] 3 | } 4 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | WEATHER_API_KEY= 2 | OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxx 3 | VAPI_BASE_URL=https://api.vapi.ai 4 | VAPI_API_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | .cache 3 | node_modules 4 | # Build directory 5 | public 6 | # Other 7 | .DS_Store 8 | yarn-error.log 9 | 10 | .vercel 11 | 12 | *.env 13 | !example.env 14 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | 3 | export default async (req: VercelRequest, res: VercelResponse) => { 4 | return res.status(200).json({message: 'hello world'}); 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enablePaths": [ 3 | "supabase/functions" 4 | ], 5 | "deno.lint": true, 6 | "deno.unstable": true, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /api/webhook/.endOfCallReport.ts: -------------------------------------------------------------------------------- 1 | import { EndOfCallReportPayload } from "../../types/vapi.types"; 2 | 3 | export const endOfCallReportHandler = async ( 4 | payload?: EndOfCallReportPayload 5 | ): Promise => { 6 | /** 7 | * Handle Business logic here. 8 | * You can store the information like summary, typescript, recordingUrl or even the full messages list in the database. 9 | */ 10 | 11 | return; 12 | }; 13 | -------------------------------------------------------------------------------- /functions/index.ts: -------------------------------------------------------------------------------- 1 | import { findKeywords } from "./fetchKeyword"; 2 | import { getCharacterInspiration } from "./getCharacterInspiration"; 3 | import { getRandomName } from "./getRandomName"; 4 | import { getWeather } from "./weather"; 5 | 6 | export default { 7 | getWeather: getWeather, 8 | findKeywords: findKeywords, 9 | getRandomName: getRandomName, 10 | getCharacterInspiration: getCharacterInspiration, 11 | }; 12 | -------------------------------------------------------------------------------- /config/env.config.ts: -------------------------------------------------------------------------------- 1 | export const envConfig = { 2 | weather: { 3 | baseUrl: 4 | process.env.WEATHER_BASE_URL ?? `https://api.openweathermap.org/data/2.5`, 5 | apiKey: process.env.WEATHER_API_KEY ?? ``, 6 | }, 7 | openai: { 8 | apiKey: process.env.OPENAI_API_KEY ?? ``, 9 | }, 10 | vapi: { 11 | baseUrl: process.env.VAPI_BASE_URL ?? "https://api.vapi.ai", 12 | apiKey: process.env.VAPI_API_KEY ?? "", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /api/webhook/.hang.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HangMessageResponse, 3 | HangPayload 4 | } from "../../types/vapi.types"; 5 | 6 | export const HangEventHandler = async ( 7 | payload?: HangPayload 8 | ): Promise => { 9 | /** 10 | * Handle Business logic here. 11 | * Sent once the call is terminated by user. 12 | * You can update the database or have some followup actions or workflow triggered. 13 | */ 14 | 15 | return {}; 16 | }; 17 | -------------------------------------------------------------------------------- /api/webhook/.transcript.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TranscriptMessageResponse, 3 | TranscriptPayload, 4 | } from "../../types/vapi.types"; 5 | 6 | export const transcriptHandler = async ( 7 | payload?: TranscriptPayload 8 | ): Promise => { 9 | /** 10 | * Handle Business logic here. 11 | * Sent during a call whenever the transcript is available for certain chunk in the stream. 12 | * You can store the transcript in your database or have some other business logic. 13 | */ 14 | 15 | return {}; 16 | }; 17 | -------------------------------------------------------------------------------- /utils/cors.utils.ts: -------------------------------------------------------------------------------- 1 | import { VercelResponse } from "@vercel/node"; 2 | 3 | export const setCors = (res: VercelResponse) => { 4 | res.setHeader("Access-Control-Allow-Credentials", "true"); 5 | res.setHeader("Access-Control-Allow-Origin", "*"); 6 | res.setHeader( 7 | "Access-Control-Allow-Methods", 8 | "GET,OPTIONS,POST,PATCH,DELETE,PUT" 9 | ); 10 | res.setHeader( 11 | "Access-Control-Allow-Headers", 12 | "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /functions/fetchKeyword.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | interface KeywordParams { 4 | keyword: string; 5 | topic?: string; 6 | } 7 | 8 | export const findKeywords = (opts: KeywordParams) => { 9 | return axios 10 | .get(`https://api.datamuse.com/words`, { 11 | params: { 12 | ml: opts.keyword, 13 | topics: opts.topic, 14 | }, 15 | }) 16 | .then( 17 | (response) => 18 | response.data 19 | .map((item) => item.word) 20 | .slice(0, Math.min(response.data.length, 10)) ?? [] 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /api/webhook/.speechUpdateHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SpeechUpdateMessageResponse, 3 | SpeechUpdatePayload, 4 | } from "../../types/vapi.types"; 5 | 6 | export const speechUpdateHandler = async ( 7 | payload?: SpeechUpdatePayload 8 | ): Promise => { 9 | /** 10 | * Handle Business logic here. 11 | * Sent during a speech status update during the call. It also lets u know who is speaking. 12 | * You can enable this by passing "speech-update" in the serverMessages array while creating the assistant. 13 | */ 14 | 15 | return {}; 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "outDir": "./dist", 12 | "rootDir": "./", 13 | "noEmitOnError": true, 14 | "isolatedModules": true 15 | }, 16 | "include": [ 17 | "api/**/*", 18 | "config/**/*", 19 | "data/**/*", 20 | "functions/**/*", 21 | "utils/**/*", 22 | "types/**/*" 23 | , "api/webhook/.*.ts" ], 24 | "exclude": ["node_modules", "dist"] 25 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-side-example-serverless-vercel", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "vercel dev", 7 | "deploy:prod": "vercel deploy --prod", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "kaustubh", 12 | "license": "ISC", 13 | "description": "", 14 | "devDependencies": { 15 | "@types/node": "^20.11.10", 16 | "@vercel/node": "^3.0.17", 17 | "typescript": "^5.3.3", 18 | "vercel": "^33.3.0" 19 | }, 20 | "dependencies": { 21 | "axios": "^1.6.7", 22 | "llamaindex": "^0.1.3", 23 | "openai": "^4.26.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /api/webhook/.statusUpdate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StatusUpdateMessageResponse, 3 | StatusUpdatePayload, 4 | } from "../../types/vapi.types"; 5 | 6 | export const statusUpdateHandler = async ( 7 | payload?: StatusUpdatePayload 8 | ): Promise => { 9 | /** 10 | * Handle Business logic here. 11 | * Sent during a call whenever the status of the call has changed. 12 | * Possible statuses are: "queued","ringing","in-progress","forwarding","ended". 13 | * You can have certain logic or handlers based on the call status. 14 | * You can also store the information in your database. For example whenever the call gets forwarded. 15 | */ 16 | 17 | return {}; 18 | }; 19 | -------------------------------------------------------------------------------- /functions/weather.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { envConfig } from "../config/env.config"; 3 | 4 | interface getWeatherParams { 5 | city: string; 6 | } 7 | 8 | export const getWeather = async ({ city }: getWeatherParams): Promise => { 9 | const fallbackResponse = { 10 | result: 11 | "Could you please tell me the name of your city again? I wasn't able to retrieve the weather data previously. I'll use this information to provide you with the latest weather updates", 12 | }; 13 | if (!city) { 14 | return fallbackResponse; 15 | } 16 | const url = `${envConfig.weather.baseUrl}/weather?q=${city}&appid=${envConfig.weather.apiKey}&units=metric`; 17 | try { 18 | const response = await axios.get(url); 19 | const weather = response.data.weather[0]; 20 | return { result: weather.description }; 21 | } catch (error) { 22 | console.error("Error fetching weather data:", error); 23 | return fallbackResponse; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /functions/getRandomName.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const nats = [ 4 | "AU", 5 | "CA", 6 | "FR", 7 | "IN", 8 | "IR", 9 | "MX", 10 | "NL", 11 | "NO", 12 | "NZ", 13 | "RS", 14 | "TR", 15 | "US", 16 | ]; 17 | 18 | interface NameParams { 19 | gender?: "male" | "female"; 20 | nat?: (typeof nats)[number]; 21 | } 22 | 23 | export const getRandomName = async (params: NameParams) => { 24 | let nat = 25 | params.nat && !nats.includes(params.nat.toUpperCase()) 26 | ? nats[Math.floor(Math.random() * nats.length)] 27 | : params.nat ?? ""; 28 | 29 | try { 30 | const results = await axios.get(`https://randomuser.me/api/`, { 31 | params: { 32 | ...params, 33 | nat, 34 | }, 35 | }); 36 | 37 | const name = results.data.results[0].name; 38 | console.log("results", params, name); 39 | return { 40 | result: name.first + " " + name.last, 41 | }; 42 | } catch (err) { 43 | throw new Error("Error fetching random name"); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vapi AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /functions/getCharacterInspiration.ts: -------------------------------------------------------------------------------- 1 | import { SimpleDirectoryReader, VectorStoreIndex } from "llamaindex"; 2 | import path from "path"; 3 | 4 | interface GetCharacterInspirationParams { 5 | inspiration: string; 6 | } 7 | 8 | export const getCharacterInspiration = async ({ 9 | inspiration, 10 | }: GetCharacterInspirationParams) => { 11 | const fallbackResponse = { 12 | result: 13 | "Sorry, I am dealing with a technical issue at the moment, perhaps because of heightened user traffic. Come back later and we can try this again. Apologies for that.", 14 | }; 15 | if (inspiration) { 16 | try { 17 | const documents = await new SimpleDirectoryReader().loadData({ 18 | directoryPath: path.join(__dirname, "../data"), 19 | }); 20 | 21 | const index = await VectorStoreIndex.fromDocuments(documents); 22 | 23 | const queryEngine = index.asQueryEngine(); 24 | const response = await queryEngine.query({ query: inspiration }); 25 | 26 | return { result: response.response, forwardToClientEnabled: true }; 27 | } catch (error) { 28 | console.log("error", error); 29 | return fallbackResponse; 30 | } 31 | } else { 32 | return fallbackResponse; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /api/webhook/.functionCall.ts: -------------------------------------------------------------------------------- 1 | import defaultFunctions from "../../functions"; 2 | import { FunctionCallPayload } from "../../types/vapi.types"; 3 | 4 | export const functionCallHandler = async ( 5 | payload: FunctionCallPayload, 6 | functions: Record = defaultFunctions 7 | ) => { 8 | /** 9 | * Handle Business logic here. 10 | * You can handle function calls here. The payload will have function name and parameters. 11 | * You can trigger the appropriate function based your requirements and configurations. 12 | * You can also have a set of validators along with each functions which can be used to first validate the parameters and then call the functions. 13 | * Here Assumption is that the function are handling the fallback cases as well. They should return the appropriate response in case of any error. 14 | */ 15 | 16 | const { functionCall } = payload; 17 | 18 | if (!functionCall) { 19 | throw new Error("Invalid Request."); 20 | } 21 | 22 | const { name, parameters } = functionCall; 23 | console.log(name, parameters); 24 | if (Object.prototype.hasOwnProperty.call(functions, name)) { 25 | return await functions[name](parameters); 26 | } else { 27 | return; 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /api/custom-llm/basic.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import OpenAI from "openai"; 3 | import { envConfig } from "../../config/env.config"; 4 | import { setCors } from "../../utils/cors.utils"; 5 | 6 | const openai = new OpenAI({ apiKey: envConfig.openai.apiKey }); 7 | 8 | export default async (req: VercelRequest, res: VercelResponse) => { 9 | setCors(res); 10 | 11 | try { 12 | const { 13 | model, 14 | messages, 15 | max_tokens, 16 | temperature, 17 | stream, 18 | call, 19 | ...restParams 20 | } = req.body; 21 | const response = { 22 | id: "chatcmpl-8mcLf78g0quztp4BMtwd3hEj58Uof", 23 | object: "chat.completion", 24 | created: Math.floor(Date.now() / 1000), 25 | model: "gpt-3.5-turbo-0613", 26 | system_fingerprint: null, 27 | choices: [ 28 | { 29 | index: 0, 30 | delta: { content: messages?.[messages.length - 1]?.content ?? "" }, 31 | logprobs: null, 32 | finish_reason: "stop", 33 | }, 34 | ], 35 | }; 36 | res.setHeader("Content-Type", "text/event-stream"); 37 | res.setHeader("Cache-Control", "no-cache"); 38 | res.setHeader("Connection", "keep-alive"); 39 | res.write(`data: ${JSON.stringify(response)}\n\n`); 40 | res.end(); 41 | 42 | // res.status(201).json(response); 43 | } catch (e) { 44 | console.log(e); 45 | res.status(500).json({ error: e }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /api/outbound.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { setCors } from "../utils/cors.utils"; 3 | import axios from "axios"; // Make sure to install axios if not already installed 4 | import { envConfig } from "../config/env.config"; 5 | 6 | export default async (req: VercelRequest, res: VercelResponse) => { 7 | if (req.method === "POST") { 8 | setCors(res); 9 | 10 | // Extract phoneNumberId, assistantId, and customerNumber from the request body 11 | const { phoneNumberId, assistantId, customerNumber } = req.body; 12 | 13 | try { 14 | /**!SECTION 15 | * Handle Outbound Call logic here. 16 | * This can initiate an outbound call to a customer's phonenumber using Vapi. 17 | */ 18 | const response = await axios.post( 19 | `${envConfig.vapi.baseUrl}/call/phone`, 20 | { 21 | phoneNumberId: phoneNumberId, 22 | assistantId: assistantId, 23 | customer: { 24 | number: customerNumber, 25 | }, 26 | }, 27 | { 28 | headers: { 29 | Authorization: `Bearer ${envConfig.vapi.apiKey}`, 30 | }, 31 | } 32 | ); 33 | res.status(200).json(response.data); 34 | } catch (error) { 35 | res.status(500).json({ 36 | message: "Failed to place outbound call", 37 | error: error.message, 38 | }); 39 | } 40 | } else { 41 | res.status(404).json({ message: "Not Found" }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "src": "/", 5 | "dest": "/api/index.ts", 6 | "methods": ["GET"] 7 | }, 8 | { 9 | "src": "/api/webhook", 10 | "dest": "/api/webhook/index.ts", 11 | "methods": ["POST"] 12 | }, 13 | { 14 | "src": "/api/webhook/(?[A-Za-z0-9-_]+)", 15 | "dest": "/api/webhook/index.ts?conversation_uuid=$1", 16 | "methods": ["POST"] 17 | }, 18 | { 19 | "src": "/api/functions/basic", 20 | "dest": "/api/functions/basic.ts", 21 | "methods": ["POST"] 22 | }, 23 | { 24 | "src": "/api/functions/rag", 25 | "dest": "/api/functions/rag.ts", 26 | "methods": ["POST"] 27 | }, 28 | { 29 | "src": "/api/functions/inbound", 30 | "dest": "/api/functions/inbound.ts", 31 | "methods": ["POST"] 32 | }, 33 | { 34 | "src": "/api/functions/outbound", 35 | "dest": "/api/functions/outbound.ts", 36 | "methods": ["POST"] 37 | }, 38 | { 39 | "src": "/api/custom-llm/basic/chat/completions", 40 | "dest": "/api/custom-llm/basic.ts", 41 | "methods": ["POST"] 42 | }, 43 | { 44 | "src": "/api/custom-llm/openai-sse/chat/completions", 45 | "dest": "/api/custom-llm/openai-sse.ts", 46 | "methods": ["POST"] 47 | }, 48 | { 49 | "src": "/api/custom-llm/openai-advanced/chat/completions", 50 | "dest": "/api/custom-llm/openai-advanced.ts", 51 | "methods": ["POST"] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /api/functions/rag.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { getCharacterInspiration } from "../../functions/getCharacterInspiration"; 3 | import { VapiPayload, VapiWebhookEnum } from "../../types/vapi.types"; 4 | import { setCors } from "../../utils/cors.utils"; 5 | 6 | /** 7 | * Handles POST requests from Vapi to perform function calls. 8 | * Specifically, it processes the `getCharacterInspiration` function call, which fetches character inspiration using a custom algorithm. 9 | * If the function call is valid and the name matches 'getCharacterInspiration', it executes the function and returns the result. 10 | * If the function name is not found, it logs an error message and throws an exception indicating the function is not found. 11 | * This handler is an example of how to implement function calls in Vapi for the 'getCharacterInspiration' functionality. 12 | */ 13 | export default async (req: VercelRequest, res: VercelResponse) => { 14 | if (req.method === "POST") { 15 | setCors(res); 16 | const payload = req.body.message as VapiPayload; 17 | 18 | if (payload.type === VapiWebhookEnum.FUNCTION_CALL) { 19 | const { functionCall } = payload; 20 | 21 | if (!functionCall) { 22 | throw new Error("Invalid Request."); 23 | } 24 | 25 | const { name, parameters } = functionCall; 26 | if (name === "getCharacterInspiration") { 27 | const result = await getCharacterInspiration(parameters as any); 28 | return res.status(201).json(result); 29 | } else { 30 | console.log(`Function ${name} not found`); 31 | throw new Error(`Function ${name} not found`); 32 | } 33 | } 34 | return res.status(201).json({}); 35 | } 36 | 37 | setCors(res); 38 | return res.status(404).send("Not found"); 39 | }; 40 | -------------------------------------------------------------------------------- /api/custom-llm/openai-sse.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import OpenAI from "openai"; 3 | import { envConfig } from "../../config/env.config"; 4 | import { setCors } from "../../utils/cors.utils"; 5 | 6 | const openai = new OpenAI({ apiKey: envConfig.openai.apiKey }); 7 | 8 | export default async (req: VercelRequest, res: VercelResponse) => { 9 | if (req.method !== "POST") { 10 | return res.status(404).json({ message: "Not Found" }); 11 | } 12 | 13 | setCors(res); 14 | 15 | try { 16 | const { 17 | model, 18 | messages, 19 | max_tokens, 20 | temperature, 21 | call, 22 | stream, 23 | metadata, 24 | ...restParams 25 | } = req.body; 26 | 27 | console.log(req.body); 28 | 29 | if (stream) { 30 | const completionStream = await openai.chat.completions.create({ 31 | model: model || "gpt-3.5-turbo", 32 | ...restParams, 33 | messages, 34 | max_tokens: max_tokens || 150, 35 | temperature: temperature || 0.7, 36 | stream: true, 37 | } as OpenAI.Chat.ChatCompletionCreateParamsStreaming); 38 | 39 | res.setHeader("Content-Type", "text/event-stream"); 40 | res.setHeader("Cache-Control", "no-cache"); 41 | res.setHeader("Connection", "keep-alive"); 42 | 43 | for await (const data of completionStream) { 44 | res.write(`data: ${JSON.stringify(data)}\n\n`); 45 | } 46 | res.end(); 47 | } else { 48 | const completion = await openai.chat.completions.create({ 49 | model: model || "gpt-3.5-turbo", 50 | ...restParams, 51 | messages, 52 | max_tokens: max_tokens || 150, 53 | temperature: temperature || 0.7, 54 | stream: false, 55 | }); 56 | return res.status(200).json(completion); 57 | } 58 | } catch (e) { 59 | console.log(e); 60 | res.status(500).json({ error: e }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /api/functions/basic.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { getRandomName } from "../../functions/getRandomName"; 3 | import { VapiPayload, VapiWebhookEnum } from "../../types/vapi.types"; 4 | import { setCors } from "../../utils/cors.utils"; 5 | 6 | /** 7 | * Handles POST requests from Vapi to perform function calls. 8 | * Specifically, it processes the `getRandomName` function call, which fetches a random name using a public API. 9 | * If the function call is valid and the name matches 'getRandomName', it executes the function and returns the result. 10 | * If the function name is not found, it logs an error message and throws an exception indicating the function is not found. 11 | * This handler is a basic example of how to implement function calls in Vapi without referring to other webhook handlers. 12 | */ 13 | export default async function handler(req: VercelRequest, res: VercelResponse) { 14 | try { 15 | if (req.method === "POST") { 16 | setCors(res); 17 | const payload = req.body.message as VapiPayload; 18 | 19 | if (payload.type === VapiWebhookEnum.FUNCTION_CALL) { 20 | const { functionCall } = payload; 21 | 22 | if (!functionCall) { 23 | throw new Error("Invalid Request."); 24 | } 25 | 26 | const { name, parameters } = functionCall; 27 | console.log('functionCall', functionCall) 28 | if (name === "getRandomName") { 29 | const result = await getRandomName(parameters); 30 | return res.status(201).json(result); 31 | } else { 32 | console.log(`Function ${name} not found`); 33 | throw new Error(`Function ${name} not found`); 34 | } 35 | } 36 | 37 | return res.status(201).json({}); 38 | } 39 | 40 | return res.status(404).json({ message: "Not Found" }); 41 | } catch (err) { 42 | return res.status(500).json({ message: "Internal Server Error" }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /assistants/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mary", 3 | "model": { 4 | "provider": "openai", 5 | "model": "gpt-3.5-turbo", 6 | "temperature": 0.7, 7 | 8 | "messages": [ 9 | { 10 | "role": "system", 11 | "content": "You're Mary, an AI assistant who can help an author design characters for their story. Understand their intention and help them define the character. You can use functions if author requests something which function is designed for. For example: to generate name in case the author doesn't have any particular name in mind, you can use getRandomName function." 12 | } 13 | ], 14 | "functions": [ 15 | { 16 | "name": "getRandomName", 17 | "description": "Generates a random name based on optional gender and nationality", 18 | "parameters": { 19 | "type": "object", 20 | "properties": { 21 | "gender": { 22 | "type": "string", 23 | "enum": ["male", "female"], 24 | "description": "The gender for which to generate a name." 25 | }, 26 | "nat": { 27 | "type": "string", 28 | "description": "The nationality based on which to generate a name. Example: IN for India, US for United States of America or USA and so on." 29 | } 30 | } 31 | } 32 | }, 33 | { 34 | "name": "getCharacterInspiration", 35 | "description": "Provides character inspiration based on a given query provided by the author.", 36 | "parameters": { 37 | "type": "object", 38 | "properties": { 39 | "inspiration": { 40 | "type": "string", 41 | "description": "Based on the user query, this defines the inspiration that the author is looking for. It could be some kind of similarity or something else as well." 42 | } 43 | } 44 | } 45 | } 46 | ] 47 | }, 48 | "voice": { 49 | "provider": "11labs", 50 | "voiceId": "paula" 51 | }, 52 | "firstMessage": "Hi. I'm Mary, your personal character sketch pad." 53 | } 54 | -------------------------------------------------------------------------------- /assistants/custom-llm.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mary", 3 | "model": { 4 | "provider": "custom-llm", 5 | "model": "gpt-3.5-turbo", 6 | "url": "https://server-side-example-serverless-vercel.vercel.app/api/custom-llm/openai-sse", 7 | 8 | "temperature": 0.7, 9 | 10 | "messages": [ 11 | { 12 | "role": "system", 13 | "content": "You're Mary, an AI assistant who can help an author design characters for their story. Understand their intention and help them define the character. You can use functions if author requests something which function is designed for. For example: to generate name in case the author doesn't have any particular name in mind, you can use getRandomName function." 14 | } 15 | ], 16 | "functions": [ 17 | { 18 | "name": "getRandomName", 19 | "description": "Generates a random name based on optional gender and nationality", 20 | "parameters": { 21 | "type": "object", 22 | "properties": { 23 | "gender": { 24 | "type": "string", 25 | "enum": ["male", "female"], 26 | "description": "The gender for which to generate a name." 27 | }, 28 | "nat": { 29 | "type": "string", 30 | "description": "The nationality based on which to generate a name. Example: IN for India, US for United States of America or USA and so on." 31 | } 32 | } 33 | } 34 | }, 35 | { 36 | "name": "getCharacterInspiration", 37 | "description": "Provides character inspiration based on a given query provided by the author.", 38 | "parameters": { 39 | "type": "object", 40 | "properties": { 41 | "inspiration": { 42 | "type": "string", 43 | "description": "Based on the user query, this defines the inspiration that the author is looking for. It could be some kind of similarity or something else as well." 44 | } 45 | } 46 | } 47 | } 48 | ] 49 | }, 50 | "voice": { 51 | "provider": "11labs", 52 | "voiceId": "paula" 53 | }, 54 | "firstMessage": "Hi. I'm Mary, your personal character sketch pad." 55 | } 56 | -------------------------------------------------------------------------------- /api/webhook/index.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { VapiPayload, VapiWebhookEnum } from "../../types/vapi.types"; 3 | import { assistantRequestHandler } from "./.assistantRequest"; 4 | import { endOfCallReportHandler } from "./.endOfCallReport"; 5 | import { functionCallHandler } from "./.functionCall"; 6 | import { speechUpdateHandler } from "./.speechUpdateHandler"; 7 | import { statusUpdateHandler } from "./.statusUpdate"; 8 | import { transcriptHandler } from "./.transcript"; 9 | import { HangEventHandler } from "./.hang"; 10 | import { setCors } from "../../utils/cors.utils"; 11 | 12 | export default async (req: VercelRequest, res: VercelResponse) => { 13 | if ((req.method = "POST")) { 14 | setCors(res); 15 | const conversationUuid = req.query.conversation_uuid as string; 16 | 17 | if (conversationUuid) { 18 | // This way we can fetch some data from database and use it in the handlers. 19 | // Here you can fetch some context which will be shared accorss all the webhook events for this conversation. 20 | console.log("conversationUuid", conversationUuid); 21 | } 22 | try { 23 | const payload = req.body.message as VapiPayload; 24 | console.log("type", payload.type, payload); 25 | switch (payload.type) { 26 | case VapiWebhookEnum.FUNCTION_CALL: 27 | return res.status(201).json(await functionCallHandler(payload)); 28 | case VapiWebhookEnum.STATUS_UPDATE: 29 | return res.status(201).json(await statusUpdateHandler(payload)); 30 | case VapiWebhookEnum.ASSISTANT_REQUEST: 31 | return res.status(201).json(await assistantRequestHandler(payload)); 32 | case VapiWebhookEnum.END_OF_CALL_REPORT: 33 | return res.status(201).json(await endOfCallReportHandler(payload)); 34 | case VapiWebhookEnum.SPEECH_UPDATE: 35 | return res.status(201).json(await speechUpdateHandler(payload)); 36 | case VapiWebhookEnum.TRANSCRIPT: 37 | return res.status(201).json(await transcriptHandler(payload)); 38 | case VapiWebhookEnum.HANG: 39 | return res.status(201).json(await HangEventHandler(payload)); 40 | default: 41 | throw new Error(`Unhandled message type`); 42 | } 43 | } catch (error) { 44 | return res.status(500).send(error.message); 45 | } 46 | } 47 | 48 | return res.status(404).send("Not found"); 49 | }; 50 | -------------------------------------------------------------------------------- /api/custom-llm/openai-advanced.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import OpenAI from "openai"; 3 | import { envConfig } from "../../config/env.config"; 4 | import { setCors } from "../../utils/cors.utils"; 5 | 6 | const openai = new OpenAI({ apiKey: envConfig.openai.apiKey }); 7 | 8 | export default async (req: VercelRequest, res: VercelResponse) => { 9 | if (req.method !== "POST") { 10 | return res.status(404).json({ message: "Not Found" }); 11 | } 12 | 13 | setCors(res); 14 | 15 | try { 16 | const { 17 | model, 18 | messages, 19 | max_tokens, 20 | temperature, 21 | stream, 22 | call, 23 | ...restParams 24 | } = req.body; 25 | 26 | const lastMessage = messages?.[messages.length - 1]; 27 | const prompt = await openai.completions.create({ 28 | model: "gpt-3.5-turbo-instruct", 29 | prompt: ` 30 | Create a prompt which can act as a prompt templete where I put the original prompt and it can modify it according to my intentions so that the final modified prompt is more detailed.You can expand certain terms or keywords. 31 | ---------- 32 | PROMPT: ${lastMessage.content}. 33 | MODIFIED PROMPT: `, 34 | max_tokens: 500, 35 | temperature: 0.7, 36 | }); 37 | 38 | const modifiedMessage = [ 39 | ...messages.slice(0, messages.length - 1), 40 | { ...lastMessage, content: prompt.choices[0].text }, 41 | ]; 42 | 43 | if (stream) { 44 | const completionStream = await openai.chat.completions.create({ 45 | model: model || "gpt-3.5-turbo", 46 | ...restParams, 47 | messages: modifiedMessage, 48 | max_tokens: max_tokens || 150, 49 | temperature: temperature || 0.7, 50 | stream: true, 51 | } as OpenAI.Chat.ChatCompletionCreateParamsStreaming); 52 | res.setHeader("Content-Type", "text/event-stream"); 53 | res.setHeader("Cache-Control", "no-cache"); 54 | res.setHeader("Connection", "keep-alive"); 55 | 56 | for await (const data of completionStream) { 57 | res.write(`data: ${JSON.stringify(data)}\n\n`); 58 | } 59 | res.end(); 60 | } else { 61 | const completion = await openai.chat.completions.create({ 62 | model: model || "gpt-3.5-turbo", 63 | ...restParams, 64 | messages: modifiedMessage, 65 | max_tokens: max_tokens || 150, 66 | temperature: temperature || 0.7, 67 | stream: false, 68 | }); 69 | return res.status(200).json(completion); 70 | } 71 | } catch (e) { 72 | console.log(e); 73 | res.status(500).json({ error: e }); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /api/webhook/.assistantRequest.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionMessage, ChatCompletionSystemMessageParam } from "openai/resources"; 2 | import { 3 | AssistantRequestMessageResponse, 4 | AssistantRequestPayload, 5 | } from "../../types/vapi.types"; 6 | 7 | export const assistantRequestHandler = async ( 8 | payload?: AssistantRequestPayload 9 | ): Promise => { 10 | /**!SECTION 11 | * Handle Business logic here. 12 | * You can fetch your database to see if there is an existing assistant associated with this call. If yes, return the assistant. 13 | * You can also fetch some params from your database to create the assistant and return it. 14 | * You can have various predefined static assistant here and return them based on the call details. 15 | */ 16 | 17 | const systemMessage: ChatCompletionSystemMessageParam = { 18 | role: "system", 19 | content: 20 | "You're Paula, an AI assistant who can help the user decide what do he/she wants to watch on Broadway. User can ask you to suggest shows and book tickets. You can get the list of available shows from broadway and show them to the user, and then you can help user decide which ones to choose and which broadway theatre they can visit. After this confirm the details and book the tickets. ", 21 | } 22 | 23 | const assistant = payload.call 24 | ? { 25 | name: "Paula", 26 | model: { 27 | provider: "openai", 28 | model: "gpt-3.5-turbo", 29 | temperature: 0.7, 30 | 31 | messages: [systemMessage], 32 | functions: [ 33 | { 34 | name: "sendEmail", 35 | description: 36 | "Send email to the given email address and with the given content.", 37 | parameters: { 38 | type: "object", 39 | properties: { 40 | email: { 41 | type: "string", 42 | description: "Email to which we want to send the content.", 43 | }, 44 | content: { 45 | type: "string", 46 | description: "Actual Content of the email to be sent.", 47 | }, 48 | }, 49 | required: ["email"], 50 | }, 51 | }, 52 | ], 53 | }, 54 | voice: { 55 | provider: "11labs", 56 | voiceId: "paula", 57 | }, 58 | firstMessage: "Hi, I'm Paula, your personal email assistant.", 59 | } 60 | : null; 61 | if (assistant) return { assistant }; 62 | 63 | throw new Error(`Invalid call details provided.`); 64 | }; 65 | -------------------------------------------------------------------------------- /api/webhook/README.md: -------------------------------------------------------------------------------- 1 | # Handling Server URL Events with Vapi 2 | 3 | Welcome to the guide on handling Server URL events with Vapi. The Server URL is a webhook-like mechanism that Vapi uses to communicate with your server for various events during a call's lifecycle. 4 | 5 | ## Available Events and Their Uses 6 | 7 | ### Function Calling 8 | 9 | - Event: function-call 10 | - Description: Triggered when an assistant determines a function needs to be called based on the systemPrompt in messages. 11 | - Use Case: Use this event to perform actions like sending emails or fetching data. 12 | - Example Implementation: See `.functionCall.ts` for handling function calls. 13 | 14 | ### Retrieving Assistants 15 | 16 | - Event: assistant-request 17 | - Description: Triggered when a call comes in without a specified assistant, prompting your server to provide one. 18 | - Use Case: Dynamically assign assistants based on the caller's phone number or other criteria. 19 | - Example Implementation: See `.assistantRequest.ts` for providing an assistant. 20 | 21 | ### Call Status Updates 22 | 23 | - Event: status-update 24 | - Description: Sent during a call to update the status, such as when a call starts or ends. 25 | - Use Case: Monitor call progress or log call statuses for analytics. 26 | - Example Implementation: See `.statusUpdate.ts` for handling status updates and potentially storing them in your database or triggering other logic based on the call status. 27 | 28 | ### End of Call Report 29 | 30 | - Event: end-of-call-report 31 | - Description: Sent when a call ends, providing a summary and transcript along with other details of the call. 32 | - Use Case: Store call summaries and transcripts for record-keeping or analysis. 33 | - Example Implementation: See `.endOfCallReport.ts` for storing call reports. 34 | 35 | ### Hang Notifications 36 | 37 | - Event: hang 38 | - Description: Sent if the assistant fails to respond for 5+ seconds. 39 | - Use Case: Notify your team of potential issues or log incidents for troubleshooting. 40 | - Example Implementation: See `.hang.ts` for setting up an alert system or logging the incident. 41 | 42 | ### Speech Updates 43 | 44 | - Event: speech-update 45 | - Description: Sent during a call to provide updates on who is speaking. 46 | - Use Case: Use this event to identify speaker changes or to trigger actions based on who is speaking. 47 | - Example Implementation: See `.speechUpdateHandler.ts` for handling speech updates. 48 | 49 | ### Transcript Updates 50 | 51 | - Event: transcript 52 | - Description: Sent during a call whenever a transcript is available for a certain chunk in the stream. 53 | - Use Case: Store transcripts for analysis or real-time processing. 54 | - Example Implementation: See `.transcript.ts` for handling transcript updates. 55 | 56 | ## Dynamic Shared context. 57 | 58 | There could be scenarios where you may want to have some shared context across all the interactions b/w the Backend and the Vapi. You can do so by using a serverUrl which has a id which you can use to fetch shared context. 59 | 60 | For example in the current implementation, `/api/webhook/` can be used to configure when creating the assistant. So that whenever any webhook is triggered for that assistant you can fetch the context using conversation_uuid. 61 | -------------------------------------------------------------------------------- /api/inbound.ts: -------------------------------------------------------------------------------- 1 | import { VercelRequest, VercelResponse } from "@vercel/node"; 2 | import { setCors } from "../utils/cors.utils"; 3 | import { VapiPayload, VapiWebhookEnum } from "../types/vapi.types"; 4 | 5 | export default async (req: VercelRequest, res: VercelResponse) => { 6 | if ((req.method = "POST")) { 7 | setCors(res); 8 | try { 9 | const payload = req.body.message as VapiPayload; 10 | switch (payload.type) { 11 | case VapiWebhookEnum.ASSISTANT_REQUEST: 12 | /**!SECTION 13 | * Handle Business logic here. 14 | * This is triggered when someone calls your registered number with Vapi 15 | * You can fetch your database to see if there is an existing assistant associated with this call. If yes, return the assistant. 16 | * You can also fetch some params from your database to create the assistant and return it. 17 | * You can have various predefined static assistant here and return them based on the call details. 18 | */ 19 | 20 | const assistant = payload.call 21 | ? { 22 | name: "Paula", 23 | model: { 24 | provider: "openai", 25 | model: "gpt-3.5-turbo", 26 | temperature: 0.7, 27 | 28 | messages: [ 29 | { 30 | role: "system", 31 | content: 32 | "You're Paula, an AI assistant who can help the user decide what do he/she wants to watch on Broadway. User can ask you to suggest shows and book tickets. You can get the list of available shows from broadway and show them to the user, and then you can help user decide which ones to choose and which broadway theatre they can visit. After this confirm the details and book the tickets. ", 33 | }, 34 | ], 35 | functions: [ 36 | { 37 | name: "sendEmail", 38 | description: 39 | "Send email to the given email address and with the given content.", 40 | parameters: { 41 | type: "object", 42 | properties: { 43 | email: { 44 | type: "string", 45 | description: 46 | "Email to which we want to send the content.", 47 | }, 48 | content: { 49 | type: "string", 50 | description: 51 | "Actual Content of the email to be sent.", 52 | }, 53 | }, 54 | required: ["email"], 55 | }, 56 | }, 57 | ], 58 | }, 59 | voice: { 60 | provider: "11labs", 61 | voiceId: "paula", 62 | }, 63 | firstMessage: "Hi, I'm Paula, your personal email assistant.", 64 | } 65 | : null; 66 | if (assistant) return res.status(201).json({ assistant }); 67 | 68 | break; 69 | default: 70 | throw new Error(`Unhandled message type`); 71 | } 72 | } catch (error) { 73 | return res.status(500).send(error.message); 74 | } 75 | } 76 | 77 | return res.status(404).send("Not found"); 78 | }; 79 | -------------------------------------------------------------------------------- /api/custom-llm/README.md: -------------------------------------------------------------------------------- 1 | # Custom Large Language Models (LLM) with Vapi 2 | 3 | Welcome to the guide on integrating custom Large Language Models (LLM) with Vapi, your versatile voice assistant SDK. This guide will help you harness the power of advanced language models to enhance Vapi's capabilities and provide a more personalized experience for your users. 4 | 5 | ## Potential Use Cases 6 | 7 | Custom LLM integration in Vapi can be utilized in a variety of scenarios: 8 | 9 | - **Enhanced Conversational AI**: Improving the natural language understanding and generation for more complex and nuanced conversations. 10 | - **Domain-Specific Knowledge**: Tailoring responses based on industry-specific knowledge or expertise. 11 | - **Personalization**: Adapting language model responses to individual user preferences or history. 12 | - **Multilingual Support**: Offering support in multiple languages by leveraging LLMs trained on diverse datasets. 13 | 14 | ### When to Use Custom LLMs 15 | 16 | Consider integrating custom LLMs when: 17 | 18 | - You require more advanced natural language processing beyond Vapi's default capabilities. 19 | - Your application needs to understand and generate content in specific domains or technical fields. 20 | - You aim to provide a more personalized and context-aware user experience. 21 | - You want to support additional languages to cater to a global user base. 22 | 23 | ## Getting Started 24 | 25 | To integrate a custom LLM with Vapi, you need to provide an OpenAI-compatible POST endpoint for `/chat/completions` that Vapi will trigger. This endpoint should be capable of handling requests from Vapi which will follow [OpenAI Request format](https://platform.openai.com/docs/api-reference/chat/create) and returning responses in OpenAI compatible format that Vapi can process. In the request vapi will send `{stream: true}`. Response can be in a standard JSON format (non-streaming) or use Server-Sent Events (SSE) for real-time data streaming. Vapi is equipped to handle both response types. 26 | 27 | ## Example Server Implementations 28 | 29 | The following sections provide an overview of the example server implementations available in the codebase. These examples illustrate how to create a chat/completions endpoint that is compatible with the OpenAI API and can be integrated with Vapi to enhance its conversational capabilities using custom Large Language Models (LLM). 30 | 31 | #### Basic Endpoint Implementation 32 | 33 | The example (`/basic`) demonstrates a straightforward implementation of the `/chat/completions` endpoint. It is designed to handle POST requests from Vapi, process them using an LLM (which can be Openai/self hosted or even some other provider), and return a response in the JSON format. This basic setup is suitable for scenarios where streaming is not that important. 34 | 35 | #### Server-Sent Events (SSE) Endpoint 36 | 37 | The example at `/openai-sse` showcases an endpoint that utilizes Server-Sent Events (SSE) for real-time data streaming. SSE allows your server to push updates to the client over a single established HTTP connection. This is particularly useful for applications that require real-time interaction and continuous updates without the overhead of repeatedly polling the server and you don't want your user to wait until the full response of your llm is generated. In this example as well you can replace OpenAI with any other provider or even self hosted LLM instance. Make sure to ensure the [response format](https://platform.openai.com/docs/api-reference/chat/streaming) similar to OpenAI. 38 | 39 | #### Advanced Endpoint Implementation 40 | 41 | The advanced example provided in `/openai-advanced` includes more complex logic for handling a variety of conversational scenarios where the user prompt is being modified and then fed to the OpenAI. This might also involve maintaining conversation context across multiple interactions, integrating domain-specific knowledge, or connecting with other APIs and services. This setup is ideal for applications that need a more sophisticated understanding and more advanced use cases. 42 | 43 | Each example serves as a template that can be customized according to the specific requirements of your application. They are designed to be starting points that illustrate the integration process and provide a foundation for building a robust and personalized conversational experience with Vapi. 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vapi Example for Serverless Vercel 2 | 3 | Welcome to the Vapi Serverless Vercel sample project. This project demonstrates how you can extend the functionalities of Vapi, an abstraction layer for your personal assistant, to create a unique experience tailored for story writers. Using this project, writers can design characters for their stories through voice interactions with the assistant. 4 | 5 | ## Project Overview 6 | 7 | The project showcases the following customizations: 8 | 9 | - **Function Calling**: Writers can invoke custom functions to retrieve character inspirations and generate random names based on specific criteria. For more info [click here](api/functions/README.md) 10 | - **Custom Large Language Model (LLM) Integration**: Enhance conversational capabilities by integrating custom LLMs with Vapi for nuanced and context-aware interactions. For more info [click here](api/custom-llm/README.md) 11 | - **Server URL Events**: Handle various events during a call's lifecycle, such as function calls and assistant requests, to provide dynamic responses. For more info [click here](api/webhook/README.md) 12 | 13 | ## Features 14 | 15 | - **Creative Prompts for Character Development**: Utilize the function that provides creative prompts for character development to get inspired based on a query provided by the author. 16 | - **Random Name Generation**: Use a public endpoint to generate random names, with options to specify gender and nationality based on user input. 17 | - **Advanced Conversational Interactions**: Leverage advanced LLMs to improve natural language understanding and generation for complex conversations. 18 | 19 | ## Getting Started 20 | 21 | To get started with this project: 22 | 23 | #### Basic Project Setup 24 | 25 | 1. Clone the repository to your local machine. 26 | 2. Install the dependencies by running `pnpm install`. 27 | 3. Setup Vercel using `vercel` command from the root directory. Install vercel cli if you don't have it using `npm i -g vercel`. 28 | 4. You can start the project locally using command `pnpm start` 29 | 5. You can deploy the project to vercel using command `pnpm deploy:prod` 30 | 31 | #### Configuration 32 | 33 | 1. create a .env file in your repository using the command `cp example.env .env` 34 | 2. Get ur `OPENAI_API_KEY` from openai and update the `.env` file. 35 | 3. From Vapi dashboard, you can get your Vapi Private key from **Dashboard > Accounts > Vapi Keys > Api Key** and update `.env` file 36 | 4. Get ServerURL 37 | 38 | 1. Using Ngrok: Start the project locally using `pnpm start` and then use ngrok to get the url. 39 | 2. Using Vercel: Deploy functions to vercel using `pnpm deploy:prod` and get URL from the Vercel. 40 | 41 | The serverURL to be configured in the **Dashboard > Accounts > Settings** is `https:///api/webhook` This has all the messages placeholder. You can also try `https:///api/functions/basic` or `https:///api/rag` 42 | 43 | 5. There are sample request body in `./assistants` folder. Use them to create an assistant using the POST endpoint `https://api.vapi.ai/api/assistant` with any of the body from `./assistants`. 44 | 6. Now you have an assistant created which you can talk with from the Vapi Dashboard. 45 | 46 | #### Explore and make changes. 47 | 48 | 1. Explore the `api` directory to understand how the function calling and custom LLM integrations and webhook event handling are set up. 49 | 2. Review the types directory to see the data structures used for handling Vapi events and payloads. 50 | 3. Check the data directory for sample data that the function for creative character prompts can use. 51 | 4. Remove any unnecessary code and start adding your own logic. 52 | 53 | ## Examples 54 | 55 | Here are some examples of how the custom functionalities can be used: 56 | 57 | - A writer asks Vapi for help with character development, and Vapi responds with a creative prompt from the function designed for this purpose. 58 | - A writer requests a random name for a character, and Vapi uses the function for random name generation to provide a name with the specified gender and nationality. 59 | 60 | ## Conclusion 61 | 62 | This sample project illustrates the power of Vapi customization for specific use cases, such as assisting story writers in their creative process. By following the examples and guidelines provided, developers can create a more versatile and responsive voice assistant that caters to the unique needs of their users. 63 | 64 | For additional help and documentation, refer to the official [Vapi documentation](https://docs.vapi.ai). 65 | -------------------------------------------------------------------------------- /types/vapi.types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | ChatCompletionCreateParams, 4 | ChatCompletionMessageParam, 5 | FunctionDefinition, 6 | } from "openai/resources"; 7 | 8 | export interface Model { 9 | model: string; 10 | messages: ChatCompletionMessageParam[]; 11 | temperature?: number; 12 | functions?: { 13 | name: string; 14 | async?: boolean; 15 | description?: string; 16 | parameters?: FunctionDefinition | any; 17 | }[]; 18 | provider: string; 19 | url?: string; 20 | } 21 | 22 | const PLAY_HT_EMOTIONS = [ 23 | "female_happy", 24 | "female_sad", 25 | "female_angry", 26 | "female_fearful", 27 | "female_disgust", 28 | "female_surprised", 29 | ] as const; 30 | type PlayHTEmotion = (typeof PLAY_HT_EMOTIONS)[number]; 31 | 32 | interface Voice { 33 | provider: string; 34 | voiceId: string; 35 | speed?: number; 36 | stability?: number; 37 | similarityBoost?: number; 38 | style?: number; 39 | useSpeakerBoost?: boolean; 40 | temperature?: number; 41 | emotion?: PlayHTEmotion; 42 | voiceGuidance?: number; 43 | styleGuidance?: number; 44 | textGuidance?: number; 45 | } 46 | 47 | export interface Assistant { 48 | // Properties from AssistantUserEditable 49 | name?: string; 50 | transcriber?: { 51 | provider: "deepgram"; 52 | model?: string; 53 | keywords?: string[]; 54 | }; 55 | model?: Model; 56 | voice?: Voice; 57 | language?: string; 58 | forwardingPhoneNumber?: string; 59 | firstMessage?: string; 60 | voicemailMessage?: string; 61 | endCallMessage?: string; 62 | endCallPhrases?: string[]; 63 | interruptionsEnabled?: boolean; 64 | recordingEnabled?: boolean; 65 | endCallFunctionEnabled?: boolean; 66 | dialKeypadFunctionEnabled?: boolean; 67 | fillersEnabled?: boolean; 68 | clientMessages?: any[]; 69 | serverMessages?: any[]; 70 | silenceTimeoutSeconds?: number; 71 | responseDelaySeconds?: number; 72 | liveTranscriptsEnabled?: boolean; 73 | keywords?: string[]; 74 | parentId?: string; 75 | serverUrl?: string; 76 | serverUrlSecret?: string; 77 | 78 | // Properties from AssistantPrivileged 79 | id?: string; 80 | orgId?: string; 81 | createdAt?: Date; 82 | updatedAt?: Date; 83 | } 84 | 85 | export const VAPI_CALL_STATUSES = [ 86 | "queued", 87 | "ringing", 88 | "in-progress", 89 | "forwarding", 90 | "ended", 91 | ] as const; 92 | export type VapiCallStatus = (typeof VAPI_CALL_STATUSES)[number]; 93 | 94 | export enum VapiWebhookEnum { 95 | ASSISTANT_REQUEST = "assistant-request", 96 | FUNCTION_CALL = "function-call", 97 | STATUS_UPDATE = "status-update", 98 | END_OF_CALL_REPORT = "end-of-call-report", 99 | HANG = "hang", 100 | SPEECH_UPDATE = "speech-update", 101 | TRANSCRIPT = "transcript", 102 | } 103 | 104 | export interface ConversationMessage { 105 | role: "user" | "system" | "bot" | "function_call" | "function_result"; 106 | message?: string; 107 | name?: string; 108 | args?: string; 109 | result?: string; 110 | time: number; 111 | endTime?: number; 112 | secondsFromStart: number; 113 | } 114 | 115 | interface BaseVapiPayload { 116 | call: VapiCall; 117 | } 118 | 119 | export interface AssistantRequestPayload extends BaseVapiPayload { 120 | type: VapiWebhookEnum.ASSISTANT_REQUEST; 121 | } 122 | 123 | export interface StatusUpdatePayload extends BaseVapiPayload { 124 | type: VapiWebhookEnum.STATUS_UPDATE; 125 | status: VapiCallStatus; 126 | messages?: ChatCompletionMessageParam[]; 127 | } 128 | 129 | export interface FunctionCallPayload extends BaseVapiPayload { 130 | type: VapiWebhookEnum.FUNCTION_CALL; 131 | functionCall: ChatCompletionCreateParams.Function; 132 | } 133 | 134 | export interface EndOfCallReportPayload extends BaseVapiPayload { 135 | type: VapiWebhookEnum.END_OF_CALL_REPORT; 136 | endedReason: string; 137 | transcript: string; 138 | messages: ConversationMessage[]; 139 | summary: string; 140 | recordingUrl?: string; 141 | } 142 | 143 | export interface HangPayload extends BaseVapiPayload { 144 | type: VapiWebhookEnum.HANG; 145 | } 146 | 147 | export interface SpeechUpdatePayload extends BaseVapiPayload { 148 | type: VapiWebhookEnum.SPEECH_UPDATE; 149 | status: "started" | "stopped"; 150 | role: "assistant" | "user"; 151 | } 152 | 153 | export interface TranscriptPayload { 154 | type: VapiWebhookEnum.TRANSCRIPT; 155 | role: "assistant" | "user"; 156 | transcriptType: "partial" | "final"; 157 | transcript: string; 158 | } 159 | 160 | export interface VapiCall {} 161 | export type VapiPayload = 162 | | AssistantRequestPayload 163 | | StatusUpdatePayload 164 | | FunctionCallPayload 165 | | EndOfCallReportPayload 166 | | SpeechUpdatePayload 167 | | TranscriptPayload 168 | | HangPayload; 169 | 170 | export type FunctionCallMessageResponse = 171 | | { 172 | result: string; 173 | } 174 | | any; 175 | 176 | export interface AssistantRequestMessageResponse { 177 | assistant?: Assistant; 178 | error?: string; 179 | } 180 | 181 | export interface StatusUpdateMessageResponse {} 182 | export interface SpeechUpdateMessageResponse {} 183 | export interface TranscriptMessageResponse {} 184 | export interface HangMessageResponse {} 185 | export interface EndOfCallReportMessageResponse {} 186 | 187 | export type VapiResponse = 188 | | AssistantRequestMessageResponse 189 | | FunctionCallMessageResponse 190 | | EndOfCallReportMessageResponse 191 | | HangMessageResponse 192 | | StatusUpdateMessageResponse 193 | | SpeechUpdateMessageResponse 194 | | TranscriptMessageResponse; 195 | -------------------------------------------------------------------------------- /api/functions/README.md: -------------------------------------------------------------------------------- 1 | # Function Calling with Vapi 2 | 3 | Welcome to Vapi, your versatile voice assistant SDK. This guide will walk you through the process of extending Vapi's functionality with custom function calling, allowing you to focus on your core business logic. 4 | 5 | ## Potential Use Cases 6 | 7 | Custom function calling in Vapi can be leveraged in various scenarios to enhance the user experience and provide tailored responses. Here are some potential use cases where custom functions can be particularly beneficial: 8 | 9 | - **Data Retrieval**: Fetching real-time data from external APIs, such as weather information, stock prices, or news updates. 10 | - **User Interaction**: Handling complex user queries that require specific business logic, like booking appointments or processing orders. 11 | - **Content Generation**: Creating dynamic content based on user input, such as personalized greetings, stories, or marketing copy. 12 | - **Automation**: Performing background tasks like sending emails, updating databases, or triggering workflows in response to voice commands. 13 | - **Integration**: Connecting with third-party services or platforms to extend the functionality of Vapi, like integrating with CRM systems or IoT devices. 14 | - **Analytics**: Gathering and analyzing user data to provide insights, recommendations, or personalized experiences. 15 | 16 | ### When to Use Function Calls 17 | 18 | Function calls are a better choice when: 19 | 20 | - The response to a user query requires processing that cannot be handled within the predefined capabilities of Vapi. 21 | - There is a need to interact with external systems or databases to complete a user request. 22 | - Custom business logic is required to generate a response that is not supported by Vapi's built-in configurations or through prompt manipulations. 23 | - You want to maintain control over the data processing and logic flow within your own infrastructure. 24 | 25 | By utilizing custom function calls, developers can create a more versatile and responsive voice assistant that caters to the specific needs of their users and business. 26 | 27 | ## Getting Started 28 | 29 | To get started with function calling in Vapi, you'll need to define your functions and register them with the Vapi Assistant. These functions can be used to perform actions or fetch data in response to user queries. 30 | 31 | ## Defining Functions 32 | 33 | Create your functions in the `functions` directory. Each function can be a separate file exporting an async function that takes parameters and returns a response. 34 | 35 | Example function: 36 | 37 | ```js 38 | export const getRandomName = async ({ gender, nat }) => { 39 | // Your logic to fetch random Name 40 | return { result: "John Doe" }; 41 | }; 42 | ``` 43 | 44 | ## Registering Functions 45 | 46 | Register your functions in the `functions/index.ts` file. This file acts as a central registry for all your custom functions. 47 | 48 | Example registration: 49 | 50 | ```ts 51 | import { getCharacterInspiration } from "./getCharacterInspiration"; 52 | import { getRandomName } from "./getRandomName"; 53 | 54 | export default { 55 | getRandomName: getRandomName, 56 | getCharacterInspiration: getCharacterInspiration, 57 | // Add more functions here 58 | }; 59 | ``` 60 | 61 | ## Handling Function Calls 62 | 63 | When Vapi receives a function call, it will invoke the corresponding function from your registry. The api/webhook/functionCall.ts file is responsible for handling these calls. 64 | 65 | Example handler: 66 | 67 | ```ts 68 | import defaultFunctions from "../../functions"; 69 | 70 | export const functionCallHandler = async (payload) => { 71 | const { functionCall } = payload; 72 | const { name, parameters } = functionCall; 73 | 74 | if (defaultFunctions[name]) { 75 | return await defaultFunctions[name](parameters); 76 | } else { 77 | throw new Error(`Function ${name} not found`); 78 | } 79 | }; 80 | ``` 81 | 82 | ## Function Invocation 83 | 84 | You need to create a Vapi Assistant which will contain the function information and when to invoke it as part of the systemPrompt in the messages in the model configuration. Sample Assistant configuration is here. 85 | 86 | ```JSON 87 | { 88 | "firstMessage": "Hello, how can I help you today?", 89 | "model": { 90 | "provider": "openai", 91 | "model": "gpt-3.5-turbo", 92 | 93 | "messages": [ 94 | { 95 | "role": "system", 96 | "content": "You are a versatile assistant...", 97 | } 98 | ], 99 | "functions": [ 100 | { 101 | "name": "getRandomName", 102 | "description": "Generates a random name based on optional gender and nationality", 103 | "parameters": { 104 | "type": "object", 105 | "properties": { 106 | "gender": { "type": "string", "enum": ["male", "female"], 107 | "description": "The gender for which to generate a name." }, 108 | "nat": { "type": "string", 109 | "description": "The nationality based on which to generate a name. Example: IN for India, US for United States of America or USA and so on." } 110 | } 111 | } 112 | }, 113 | { 114 | "name": "getCharacterInspiration", 115 | "description": "Provides character inspiration based on a given query provided by the author.", 116 | "parameters": { 117 | "type": "object", 118 | "properties": { 119 | "inspiration": { "type": "string" } 120 | } 121 | } 122 | } 123 | // Add more functions here 124 | ] 125 | } 126 | } 127 | 128 | ``` 129 | 130 | ## Function Response 131 | 132 | ## Conclusion 133 | 134 | With function calling, you can easily extend the capabilities of Vapi to suit your specific business needs. By following the steps outlined above, you can integrate custom logic into your voice assistant and provide a richer experience for your users. 135 | 136 | For additional help, refer to the official [Vapi documentation](https://docs.vapi.ai) 137 | -------------------------------------------------------------------------------- /data/Characters.md: -------------------------------------------------------------------------------- 1 | Zara the Space Explorer: A fearless female astronaut in her late 30s, Zara sports a sleek, silver futuristic suit equipped with advanced technology. She has short, jet-black hair, piercing blue eyes, and a determined expression. Her personality is inquisitive and resilient, with a deep fascination for unknown galaxies. She carries a plasma rifle for defense and a holographic map device for navigation. 2 | 3 | Eldon the Wise: An elderly wizard in his 80s, Eldon possesses a long, flowing white beard and sharp, penetrating eyes. He wears deep blue robes adorned with mystical symbols that shimmer in the light. His personality is calm and knowledgeable, often sharing wisdom in riddles. He carries an ancient, glowing staff, a symbol of his magical prowess. 4 | 5 | Luna the Moon Princess: A young, ethereal girl around 16 years old, with long silver hair that seems to float like moonbeams. She has large, expressive eyes that reflect the phases of the moon. Her personality is gentle, dreamy, and compassionate, often seen speaking to animals and plants. She wears a flowing gown that glimmers like starlight, embodying the night sky. 6 | 7 | Rex the Cybernetic Warrior: A muscular man in his early 30s, Rex has undergone cybernetic enhancements, giving him a robotic arm and glowing red cybernetic eyes. His appearance is intimidating, with a scar across one cheek, and he sports a high-tech armor. His personality is stoic and focused, a soldier trained for high-stakes missions in dystopian landscapes. 8 | 9 | Aria of the Forest: A lithe, agile elf in her 200s (young for elves), Aria has long, leaf-green hair and piercing emerald eyes. She wears a cloak made of woven leaves and vines. Her personality is playful yet fierce, a protector of her forest realm. She is adept with a bow, her main weapon against those who threaten her home. 10 | 11 | Captain Flintlock: A charismatic pirate captain in his late 40s, with a rugged beard, long braided hair, and a weathered tricorn hat. He wears a coat adorned with various trinkets from his voyages. His personality is adventurous and cunning, with a love for the sea and a thirst for treasure. He wields a sword and a flintlock pistol. 12 | 13 | Ivy the Alchemist: A young woman in her mid-20s, Ivy is a genius alchemist with curly auburn hair and spectacles perched on her nose. She wears a practical apron over her dress, stained with various potion ingredients. Her personality is curious and meticulous, always experimenting with new concoctions in her cluttered laboratory. 14 | 15 | Sir Gideon the Brave: A noble knight in his late 30s, Sir Gideon is tall and muscular, with short blond hair and a square jaw. He wears shining armor and carries a broadsword and shield. His personality is honorable and courageous, driven by a strong sense of duty to protect his kingdom. 16 | 17 | Mira the Mermaid Queen: An elegant mermaid in her 50s, Mira has long, flowing aqua hair and a shimmering tail. She possesses a regal bearing, with a crown of seashells and pearls. Her personality is wise and compassionate, ruling the underwater kingdom with fairness. She communicates with sea creatures and controls water currents. 18 | 19 | Nyx the Shadow Thief: A mysterious figure in their late 20s, Nyx is lithe and agile, dressed in dark, form-fitting clothes that blend into shadows. They have short, choppy hair and a mask that obscures their face. Their personality is elusive and cunning, a master of stealth and espionage. 20 | 21 | Evelyn the Enchanted: A kind-hearted sorceress in her early 40s, Evelyn has long, wavy brunette hair and warm, brown eyes. She wears a gown that sparkles with magical energy and wields a staff topped with a crystal. Her personality is nurturing and wise, using her powers to heal and protect. 22 | 23 | Garrick the Gargoyle: A stone gargoyle come to life, Garrick is large and imposing with a chiseled, muscular appearance. His personality is stoic and observant, often mistaken for a statue when still. He guards ancient ruins and possesses the ability to glide on stone wings. 24 | 25 | Fiona the Flame Dancer: A fiery performer in her early 30s, Fiona has vibrant red hair and a passion for dance. She wears a costume of bright, flowing fabrics and performs with fire poi, captivating her audience. Her personality is lively and daring, always seeking to push the limits of her performance. 26 | 27 | Thorin the Mountain King: A dwarf king in his 150s, Thorin is stout 28 | and robust with a thick, braided beard and deep-set eyes. He wears regal armor and a heavy crown, signifying his authority. Thorin's personality is stern yet fair, deeply committed to the welfare of his mountain kingdom. He wields a mighty hammer, a symbol of his strength and leadership. 29 | 30 | Lilith the Shadow Weaver: In her late 20s, Lilith is a master of dark magic, with sleek black hair and piercing violet eyes. Her attire is elegant yet menacing, adorned with symbols of her arcane craft. Lilith's personality is enigmatic and ambitious, often seeking to expand her mystical knowledge and power. 31 | 32 | Orion the Star Hunter: A celestial hunter in his early 30s, Orion has a cosmic appearance with skin that seems to shimmer with starlight. He carries a bow that shoots arrows of pure energy. His personality is noble and righteous, dedicated to maintaining balance in the universe. 33 | 34 | Seraphina the Dragon Rider: A brave warrior in her late 20s, Seraphina has fiery red hair and scales on parts of her skin, a gift from her dragon companion. She wears armor crafted from dragon scales and exudes a fearless aura. Her personality is daring and loyal, with a deep bond to her dragon and a commitment to protecting her land from threats. 35 | 36 | Kai the Water Bender: A young man in his early 20s, Kai has the ability to manipulate water. He has a calm demeanor, with ocean-blue eyes and hair that flows like water. His clothing is light and allows for easy movement. Kai's personality is tranquil yet powerful, often using his abilities for healing and protection. 37 | 38 | Athena the Cyber Witch: In her mid-30s, Athena blends ancient witchcraft with modern technology. She has a sleek, futuristic look with neon accents in her hair and attire. Her personality is intelligent and inventive, often creating new spells and gadgets to aid in her magical endeavors. 39 | 40 | Cedric the Beast Tamer: A rugged man in his late 30s, Cedric has a strong affinity with animals, especially mythical creatures. He has a gentle yet commanding presence, with a scar across one eye. His attire is practical for his adventures in the wild. Cedric's personality is compassionate and understanding, with a deep respect for nature. 41 | 42 | Violet the Vampire Hunter: A determined woman in her late 20s, Violet is skilled in combat against supernatural beings. She has a sleek, dark appearance, with weapons specifically designed to fight vampires. Her personality is focused and relentless, driven by a personal vendetta against the undead. 43 | 44 | Jasper the Jester: A playful character in his early 20s, Jasper is a court jester with a colorful outfit and a quick wit. He is nimble and acrobatic, often using humor to diffuse tense situations. Jasper's personality is jovial and clever, hiding a sharp mind behind his fool's facade. 45 | 46 | Eloise the Elemental: A being in her 300s, Eloise embodies the elements of nature, with hair and eyes that change color to reflect her mood and powers. She wears a flowing gown that seems to be made of natural materials. Eloise's personality is wise and nurturing, often mediating conflicts between humans and nature. 47 | 48 | Magnus the Mech Pilot: A young pilot in his mid-20s, Magnus operates a powerful mech suit in battles. He has a confident demeanor, with a military-style haircut and a pilot's jumpsuit. His personality is ambitious and brave, always at the forefront of technological advancements in warfare. 49 | 50 | Nora the Nightingale: A renowned singer in her early 30s, Nora's voice has the power to enchant and heal. She dresses in elegant gowns that sparkle under the moonlight. Nora's personality is gentle and empathetic, often using her gift to bring peace and solace to those in distress. 51 | 52 | Felix the Phantom Thief: A master thief in his late 20s, Felix is known for his elusive nature and sophisticated heists. He has a charming appearance, with a sly smile and attire that blends style and stealth. Felix's personality is cunning and charismatic, always one step ahead of his pursuers. 53 | 54 | Gwendolyn the Guardian Angel: An ethereal being, Gwendolyn watches over humans with a gentle and caring demeanor. She appears in her late 20s, with wings that shimmer softly and a serene expression. Her personality is compassionate and protective, intervening subtly to guide and guard her charges. 55 | 56 | Hector the Hellhound: A fearsome creature, Hector appears as a large, black hound with eyes like burning coals. Despite his intimidating presence, his personality is loyal and discerning, often serving as a guardian for those he deems worthy. 57 | 58 | Iris the Ice Mage: A young woman in her early 20s, Iris has the power to control ice and snow. She has a pale, delicate appearance with white hair and ice-blue eyes. Her attire is made of crystalline fabrics. Iris's personality is reserved and thoughtful, often using her abilities to create stunningly beautiful ice sculptures. 59 | 60 | Jaden the Jungle Warrior: A muscular man in his early 30s, Jaden is adept at survival in the wildest terrains. He has tanned skin, with tribal tattoos and carries a spear and a shield. His personality is fierce and unyielding, with a deep connection to the natural world. 61 | 62 | Kiera the Knight Errant: A female knight in her mid-20s, Kiera roams the land seeking justice and adventure. She wears practical armor and is skilled with both sword and lance. Her personality is chivalrous and bold, often standing up for those who cannot defend themselves. 63 | 64 | Leon the Lightning Conjurer: A dynamic young man in his late teens, Leon can summon and control lightning. He has a shock of spiky hair that seems to crackle with energy and wears a suit that helps channel his powers. Leon's personality is energetic and impulsive, often rushing into action without thinking. 65 | 66 | Mina the Moon Witch: In her 50s, Mina draws her power from the moon. She has a mystical appearance, with silver hair and robes that reflect the lunar phases. Her personality is introspective and mysterious, often lost in her arcane studies. 67 | 68 | Nolan the Necromancer: A man in his late 30s, Nolan has a dark, brooding presence. He practices necromancy, often shrouded in a cloak that obscures his face. His personality is somber and complex, wrestling with the moral implications of his powers. 69 | 70 | Ophelia the Ocean Priestess: A graceful woman in her late 20s, Ophelia is a protector of the seas. She has long, flowing hair and wears a gown that mimics the ocean's colors. Her personality is serene and wise, often speaking on behalf of the creatures of the deep. 71 | 72 | Pax the Peacemaker: A diplomat in his early 40s, Pax is known for his ability to resolve conflicts. He has a calming presence, with a warm smile and attire that reflects his neutral stance. Pax's personality is empathetic and rational, always seeking to understand all sides of an argument. 73 | 74 | Quinn the Quantum Scientist: A genius in her mid-30s, Quinn explores the mysteries of quantum physics. She has a sharp, focused look, with hair often tied back in a practical manner. Her personality is intensely curious and analytical, often lost in her groundbreaking experiments. 75 | 76 | Rosalind the Rogue Assassin: A deadly assassin in her late 20s, Rosalind is skilled in stealth and combat. She has a lithe figure and wears dark, form-fitting clothing that allows for agility. Rosalind's personality is enigmatic and ruthless, with a mysterious past that drives her actions. 77 | 78 | Sylvan the Shape-Shifter: An ageless being, Sylvan has the ability to transform into different animals. His true form is androgynous and graceful, with features that hint at his animalistic abilities. Sylvan's personality is playful and cunning, often using his powers for mischief or to escape danger. 79 | 80 | Tristan the Time Traveler: A man in his early 30s, Tristan possesses a device that allows him to traverse different timelines. He has a thoughtful expression and wears a coat filled with various gadgets. Tristan's personality is adventurous and philosophical, constantly pondering the implications of his journeys through time. 81 | 82 | Ursa the Ursine Warrior: A powerful warrior in her late 30s, Ursa has bear-like features and strength. She wears armor that resembles bear fur and wields a massive battle axe. Ursa's personality is fierce and protective, especially towards her clan and family. 83 | 84 | Valora the Valkyrie: A warrior maiden in her late 20s, Valora serves in a legendary band of female warriors. She has a striking appearance, with long blonde hair and armor that shines in battle. Valora's personality is honorable and brave, committed to guiding the souls of fallen warriors. 85 | 86 | Wren the Wind Dancer: A young performer in her early 20s, Wren has the ability to manipulate air currents. She has a light, airy presence, with clothing that billows around her as she dances. Wren's personality is joyful and expressive, using her performances to tell stories and evoke emotions. 87 | 88 | Xander the Exiled Prince: A brooding young man in his early 30s, Xander was banished from his kingdom under mysterious circumstances. He has a regal yet rugged appearance, with a cloak that hides his royal lineage. Xander's personality is complex and haunted, seeking redemption and a way to reclaim his throne. 89 | 90 | Yelena the Yeti Hunter: A seasoned hunter in her late 30s, Yelena specializes in tracking mythical creatures in snowy terrains. She has a sturdy, weather-beaten appearance, with fur-lined clothing and snow goggles. Yelena's personality is pragmatic and resilient, with a deep respect for the wilderness and its mysteries. 91 | 92 | Zephyr the Zephyr Mage: A mage in his mid-20s, Zephyr controls the wind with his enchanted staff. He has a youthful, carefree look with tousled hair that seems to be constantly ruffled by a breeze. His personality is light-hearted and whimsical, often using his powers for playful purposes or to aid his travels. 93 | 94 | Amaris the Astral Seer: A mystical seer in her 50s, Amaris possesses the ability to see into the astral plane. She has a serene, ageless appearance, with eyes that seem to hold the mysteries of the cosmos. Her personality is wise and mysterious, offering guidance to those who seek her knowledge. 95 | 96 | Bryce the Beastmaster: A rugged outdoorsman in his early 40s, Bryce has the unique ability to communicate with and control animals. He has a commanding presence, with a mix of wildness and kindness in his eyes. His personality is empathetic and assertive, using his skills to maintain the balance of nature. 97 | 98 | Calista the Celestial Sorceress: In her late 20s, Calista draws her power from celestial bodies. She has a radiant appearance, with starlight twinkling in her hair and eyes. Her personality is majestic and composed, wielding her cosmic powers with grace and precision. 99 | 100 | Darius the Desert Nomad: A seasoned traveler in his mid-30s, Darius is adapted to life in harsh desert environments. He has a sun-baked complexion, with a turban and robes to protect him from the sand and sun. His personality is stoic and resourceful, skilled in survival and knowledgeable about the secrets of the desert. 101 | 102 | Detective Arthur Hale: In his mid-40s, Detective Hale is known for his sharp mind and meticulous approach to investigations. He sports a neatly trimmed beard, often wears a classic trench coat, and always has a weathered notebook in hand. His personality is a blend of old-school charm and slight cynicism, shaped by years on the force. He's respected for his dedication and is often found pondering over clues in dimly lit rooms. 103 | 104 | Samantha "Sam" Marlowe: A tech-savvy private investigator in her late 20s, Sam has a striking punk appearance with dyed hair and an array of tattoos. She's exceptionally resourceful, using the latest digital tools to gather information. Her personality is energetic, witty, and she's not afraid to bend rules for the sake of justice. Sam often works out of a cluttered, gadget-filled office and rides a vintage motorcycle. 105 | 106 | Eleanor Voss: An elegant socialite in her early 30s, Eleanor harbors a secret passion for writing crime novels. She has a sophisticated style, often seen in high fashion and with a mysterious aura. Intelligent and observant, she uses her social gatherings to gather material for her stories. Her personality is a mix of charm and keen insight, making her a favorite in high society circles, yet she retains a certain distance, always watching and noting details. 107 | 108 | Inspector Leo Ramirez: A committed and resilient police inspector in his late 30s, Leo has a commanding presence with a strong build and an earnest expression. He's known for his unwavering sense of justice and often works tirelessly to solve cases. His dedication sometimes comes at the cost of his personal life. He's well-respected among his peers and is often seen deep in discussion with his team in the bustling precinct. 109 | 110 | Agatha Christie "Ace" Greenwood: A retired intelligence officer turned private detective in her 50s, "Ace" is known for her unparalleled analytical skills and unorthodox methods. She has short, graying hair, a sharp gaze, and often dresses in practical, yet stylish attire. Her personality combines a no-nonsense attitude with unexpected warmth. She operates from a cozy, book-lined office, taking on cases that pique her interest. 111 | 112 | Spectra: Real name Mia Wang, Spectra is a young physicist who gained the ability to manipulate light after a lab accident. She's in her late 20s, with a sleek, luminous superhero costume reflecting her powers. Her personality is bright, optimistic, and she's driven by a desire to use her powers for the greater good. She balances her life as a scientist and a superhero, often facing challenges that test her both intellectually and ethically. 113 | 114 | Guardian Hawk: Tomás Rivera, a former soldier, becomes Guardian Hawk after an encounter with a mystical artifact. In his mid-30s, he has a powerful build and wears armor that resembles a hawk, complete with a winged jetpack. He's serious and disciplined, with a deep sense of duty. He patrols the city, using his military skills and new-found powers to combat threats. 115 | 116 | Nebula: Stella Freeman, a high school teacher who gained cosmic powers from a meteor shower, is Nebula. She's in her early 40s, with a costume adorned with stars and nebulas. Her personality is nurturing yet fierce when protecting her students or the world. She struggles to balance her normal life with the cosmic responsibilities thrust upon her. 117 | 118 | Iron Pulse: Engineer Lucas Kim developed a suit of powered armor to fight crime after his brother was injured in a gang attack. In his late 20s, he's a genius inventor with a suit that has various technological gadgets. He's resourceful and a bit of a loner, focusing more on his mission than personal relationships. His battles often lead him to confront not just criminals but also ethical dilemmas regarding technology and justice. 119 | 120 | Mystica: Diana Lopez is Mystica, a librarian who discovered an ancient tome that granted her magical abilities. In her mid-30s, she wears a costume that combines elements of classic sorcery with a modern flair. She's intellectual and curious, often using her vast knowledge to decipher mystical threats. Despite her powerful abilities, she constantly learns the extent of her magic, balancing her quiet librarian life with her secret identity. 121 | 122 | Eleanor of Aquitaine: A noblewoman in the 12th century, Eleanor is strong-willed, intelligent, and politically savvy. She's involved in the complex politics of medieval Europe, striving to secure her position and that of her family. She's known for her elegant attire, sharp wit, and a keen interest in the arts. 123 | 124 | Captain James Hawkins: A British naval officer in the early 19th century, Captain Hawkins is a seasoned sea captain known for his bravery and strict sense of duty. He's tall, with a commanding presence, often seen in his naval uniform adorned with medals. His personality is stern but fair, and he's deeply respected by his crew. 125 | 126 | Isabella Rossi: A Renaissance artist in Florence, Isabella is a young woman fighting to make her name in a male-dominated field. She's talented, with a passion for painting and a dream of earning recognition for her work. She often dresses modestly to blend into her surroundings, allowing her art to take center stage. 127 | 128 | Jonathan Smythe: An English explorer during the Victorian era, Jonathan is an adventurous soul, always seeking new horizons. He's in his late 30s, with rugged good looks and attire suitable for exploration. His personality is charismatic and slightly impulsive, often leading him into unforeseen adventures. 129 | 130 | Madame Genevieve: A French courtesan in the 18th century, Genevieve is known for her beauty, wit, and influence in high society. She navigates the complex social networks of her time, using her intelligence and charm to gain power and protect her independence. She's stylish, with a taste for luxury, but also possesses a sharp mind and a resilient spirit. 131 | 132 | Emma Hartley: A young bookstore owner in a small town, Emma is in her late 20s, with a love for romance novels and a hope for her own love story. She's kind, a bit shy, and has a quaint sense of style that reflects her love for classic literature. 133 | 134 | Liam Foster: A successful chef in a big city, Liam is in his early 30s, known for his culinary talents and his charming smile. He's ambitious and a bit of a perfectionist, but also has a warm heart. He often struggles to balance his demanding career with his personal life. 135 | 136 | Sophie Turner: A wedding planner in her mid-30s, Sophie is organized, creative, and always in control. She has an eye for detail and a passion for creating perfect moments, but her own love life is a series of missed connections and unfulfilled dreams. 137 | 138 | Alex Reed: A travel photographer in his late 20s, Alex is adventurous, free-spirited, and often on the move. He's handsome, with a laid-back style, capturing stunning landscapes and moments. His lifestyle makes it difficult for him to maintain long-term relationships, though he longs for a deeper connection. 139 | 140 | Julia Martinez: A high school teacher in her early 40s, Julia is widowed and finds love unexpectedly when she reconnects with an old friend. She's gentle, compassionate, and finds joy in her students and her garden. Her journey is one of rediscovering love and the courage to embrace it. 141 | 142 | Jack "Hawk" Thornton: A rugged adventurer in his mid-30s, Jack is known as "Hawk" for his sharp instincts. He's an expert in survival skills, with a history of exploring the most remote and dangerous parts of the world. He's well-built, with a casual, practical sense of style, and always ready for the next challenge. 143 | 144 | Nina Rodriguez: A daring photojournalist in her late 20s, Nina travels to war zones and conflict areas, capturing powerful images. She's brave, committed to uncovering the truth, and often finds herself in perilous situations. Her personality is a mix of fearlessness and empathy. 145 | 146 | Marco Bellini: An Italian archaeologist in his early 40s, Marco is passionate about uncovering ancient civilizations. He's knowledgeable, enthusiastic, and often embarks on expeditions to unearth historical secrets. He's well-dressed in a scholarly way, with an air of excitement about his discoveries. 147 | 148 | Zara Ahmed: A young pilot and explorer in her late 20s, Zara flies a small plane to remote areas for both scientific research and rescue missions. She's skilled, confident, and has a love for the skies. Her life is a blend of high-stakes adventures and the meticulous planning required for her missions. 149 | 150 | Ethan Chase: A former special forces operative turned treasure hunter in his mid-30s, Ethan is tough, resourceful, and has a deep knowledge of ancient artifacts and myths. He's often in conflict with rival hunters and faces moral dilemmas regarding the treasures he seeks. 151 | -------------------------------------------------------------------------------- /data/Activities.md: -------------------------------------------------------------------------------- 1 | Outdoor Sports (Soccer, Baseball): Ideal in clear skies or with few clouds, outdoor sports like soccer and baseball are perfect for engaging with teammates, friends, or family. These activities require open fields and specific sports equipment. They offer a great way to enjoy the outdoors, bond over teamwork, and maintain physical fitness. Organizing these activities is often done through local sports clubs or informal community groups. The weather plays a key role, as a clear day ensures a dry playing field and comfortable conditions for players and spectators. 2 | 3 | Indoor Sports (Basketball, Gymnastics): Suitable for any weather, indoor sports provide a consistent environment for activities like basketball and gymnastics. These sports can be enjoyed individually or as part of a team, and they require access to specialized indoor facilities. Ideal for maintaining fitness regardless of the weather outside, these sports offer opportunities for both competitive play and personal skill development. Indoor sports are a great choice for those who prefer a controlled environment, away from the unpredictability of outdoor weather conditions. 4 | 5 | Picnics/Outdoor Dining: Best enjoyed under a clear sky or amidst few clouds, picnics and outdoor dining are perfect for families, friends, or romantic outings. These activities are contingent on having a pleasant outdoor setting, such as a park or a beach. The weather significantly enhances the experience, as a sunny day adds to the enjoyment of eating outdoors and appreciating nature. Picnicking requires planning around the weather to ensure a comfortable and enjoyable experience without the interruption of rain or strong winds. 6 | 7 | Hiking/Trail Walking: Ideal in clear skies or with a few clouds, hiking and trail walking are excellent for nature enthusiasts, solo adventurers, or groups seeking outdoor exercise. Weather plays a significant role, as clear conditions ensure safer trails and more enjoyable views. Suitable for various fitness levels, these activities require appropriate footwear and knowledge of the trail conditions, which can vary with recent weather. 8 | 9 | Beach Activities (Sunbathing, Swimming): Perfect under a clear sky, beach activities like sunbathing and swimming are enjoyable with friends, family, or solo. The weather is crucial, as sunny and warm conditions enhance the beach experience, making the ocean more inviting for swimming and the sand ideal for sunbathing. Activities might include building sandcastles, playing beach volleyball, or simply relaxing by the sea. 10 | 11 | Skiing/Snowboarding: Best in light snow conditions, skiing and snowboarding are thrilling for individuals or groups who enjoy winter sports. The weather is a key factor, with fresh snow providing ideal conditions for these activities. They require access to a ski resort and specialized equipment. These activities are perfect for those who enjoy the exhilaration of cold-weather sports and mountainous landscapes. 12 | 13 | Photography (Landscape/Nature): Suitable in a variety of conditions including clear skies, few clouds, scattered clouds, or overcast weather. Ideal for solo hobbyists or photography groups. Weather conditions significantly influence the type of photography; clear skies are perfect for astrophotography, while overcast conditions offer diffused light for nature shots. Requires camera equipment and a keen eye for detail. 14 | 15 | Fishing: Best in clear skies, few clouds, or light rain, suitable for solo enthusiasts or small groups. The weather can affect fish behavior, making some conditions more favorable for a catch. Requires patience, fishing gear, and often a fishing license. Ideal for those seeking relaxation and the satisfaction of catching their own meal. 16 | 17 | Gardening: Preferable in overcast clouds or light rain, gardening is a peaceful activity for individuals or families. The weather conditions are crucial for plant growth and can make the activity more enjoyable. It’s a rewarding pastime that requires gardening tools, knowledge of plants, and a commitment to regular care. 18 | 19 | Stargazing/Astronomy: Clear skies are essential for stargazing or astronomy, making it a fascinating activity for individuals or groups interested in celestial phenomena. Requires a telescope or binoculars for a better experience. Ideal in rural areas away from city lights, this activity is perfect for night-time exploration of the stars and planets. 20 | 21 | Reading Outdoors: Enjoyable in clear skies, few clouds, or scattered clouds. Perfect for solo relaxation or as a quiet group activity. Weather conditions greatly influence the comfort of outdoor reading. A sunny day in a serene location like a park, garden, or beach enhances the experience. 22 | 23 | Barbecue/Outdoor Cooking: Best under clear skies or few clouds, suitable for family gatherings, social events, or as a culinary hobby. Weather plays a significant role as dry, warm conditions are ideal for cooking and dining outdoors. Requires grilling equipment and can be a delightful way to enjoy good food and company. 24 | 25 | Bird Watching/Nature Walks: Ideal in clear skies, few clouds, or scattered clouds. Perfect for nature lovers, bird enthusiasts, or as a peaceful solo activity. Weather influences visibility and bird activity; a clear day can enhance the experience. Requires binoculars and possibly a guidebook. This activity is often best at dawn or dusk, offering a tranquil connection with nature. 26 | 27 | Cycling/Mountain Biking: Best under clear skies or with few clouds. Suitable for fitness enthusiasts, outdoor adventurers, or families. Weather conditions affect trail quality and safety. Requires a bicycle and safety gear. This activity offers a great combination of exercise and exploration of scenic paths or rugged trails. 28 | 29 | Running/Jogging: Suitable in clear skies, few clouds, or light rain. Ideal for individuals or running groups. The weather can influence the running experience; a cool, clear day is often preferred, but light rain can add a refreshing element. Important to wear appropriate attire for weather conditions and stay hydrated. 30 | 31 | Sailing/Boating: Best in clear skies or with few clouds. Perfect for water sports enthusiasts or as a group activity. Weather conditions, especially wind and wave patterns, are crucial for a safe and enjoyable experience. Requires access to a boat and knowledge of sailing or boating techniques. This activity offers a unique perspective on nature and an opportunity for relaxation or adventure. 32 | 33 | Ice Skating: Ideal in light snow conditions. Suitable for families, couples, or solo skaters. Weather plays a role in outdoor rink conditions. Requires skates and possibly lessons for beginners. This winter activity is a festive way to enjoy the colder months, offering both exercise and leisure. 34 | 35 | Snowman Building/Snowball Fight: Best with light snow. Fun for families, children, or friends. The weather condition, particularly the type of snow, influences the activity; wetter snow is better for building snowmen and having snowball fights. A playful way to enjoy winter outdoors, fostering creativity and camaraderie. 36 | 37 | Surfing/Water Sports: Ideal in clear skies and few clouds. Perfect for adventure seekers or beachgoers. Weather, especially wave conditions, is crucial. Requires specific gear like surfboards or jet skis and often lessons for beginners. These activities offer an adrenaline rush and a unique way to experience the ocean. 38 | 39 | Museum/Indoor Exhibits: Suitable for any weather, perfect for history buffs, art lovers, or families. Indoor setting provides a weather-proof cultural experience. Offers educational opportunities and a chance to appreciate historical artifacts or artistic works. 40 | 41 | Cinema/Movie Theater: Good for any weather, ideal for movie enthusiasts, friends, or families seeking entertainment. The indoor environment offers a break from any outdoor weather conditions, providing a space for relaxation and enjoyment of films. 42 | 43 | Shopping (Indoor Malls): Suitable for any weather. Ideal for shoppers, friends, or families. Indoor malls offer a weather-independent environment for leisure, shopping, and dining. A good option for staying entertained and comfortable regardless of outdoor conditions. 44 | 45 | Spa/Wellness Center: Good for any weather. Perfect for individuals or couples seeking relaxation. The indoor setting provides a sanctuary from any external weather, focusing on health, beauty, and relaxation treatments. 46 | 47 | Concerts/Theater (Indoor): Suitable for any weather. Ideal for music and art enthusiasts. Indoor venues provide a weather-proof setting for enjoying live performances, plays, or concerts. Offers a culturally enriching experience regardless of outdoor conditions. 48 | 49 | Coffee Shop/Reading Indoors: Good for any weather. Ideal for solo visitors or small groups. Offers a cozy environment for reading, working, or socializing, irrespective of the weather outside. Coffee shops often provide a warm, inviting atmosphere for relaxation and contemplation. 50 | 51 | Cooking/Baking at Home: Suitable for any weather, perfect for food enthusiasts, families, or couples. Indoor cooking or baking offers a delightful way to spend time, regardless of outdoor conditions. It's an opportunity for creativity, learning new recipes, and enjoying homemade meals. 52 | 53 | DIY Projects/Crafts at Home: Good for any weather. Ideal for creative individuals, families, or groups looking for a productive indoor activity. Whether it's crafting, woodworking, or upcycling, this activity provides a sense of accomplishment and a personalized touch to your environment. 54 | 55 | Video Games/Indoor Entertainment: Suitable for any weather. Perfect for gamers, friends, or families. Indoor gaming provides entertainment and social interaction, unaffected by outdoor conditions. Offers a variety of genres and platforms for all ages and interests. 56 | 57 | Yoga/Meditation (Indoors): Good for any weather. Ideal for individuals seeking relaxation, fitness, or spiritual growth. Indoor yoga or meditation offers a tranquil environment for practice, away from the distractions and variability of outdoor weather. 58 | 59 | Storm Watching (Safely Indoors): Best during thunderstorms, heavy rain, or squalls. Suitable for weather enthusiasts or curious observers. Watching storms from a safe, indoor location can be awe-inspiring and educational. It requires caution and a good view, preferably from a secure window or enclosed porch. 60 | 61 | Kayaking/Canoeing: Best in clear skies or few clouds. Perfect for adventure seekers or nature lovers. Weather conditions affect water currents and visibility. Requires access to a kayak or canoe, and safety gear. Offers a peaceful or exhilarating experience on the water, depending on the environment. 62 | 63 | Rock Climbing (Outdoor): Ideal in clear skies or with few clouds. Suitable for fitness enthusiasts or adventurers. Weather significantly impacts safety and grip on surfaces. Requires proper equipment and training. Offers a challenging and rewarding physical activity, often with stunning views. 64 | 65 | Birdhouse Building: Preferable in overcast clouds or light rain. Great for families, bird watchers, or DIY enthusiasts. Mild weather conditions make working outdoors more comfortable. This activity encourages wildlife observation and provides a habitat for birds. 66 | 67 | Outdoor Yoga/Meditation: Best in clear skies, few clouds, or scattered clouds. Ideal for yoga practitioners or those seeking a peaceful retreat. Weather influences the ambiance of the practice. Requires a mat and possibly an instructor. Offers a serene way to connect with nature and oneself. 68 | 69 | Paddleboarding: Ideal in clear skies and few clouds. Suitable for water sports enthusiasts or beginners. Calm weather and water conditions are crucial for safety and enjoyment. Requires a paddleboard and possibly a lesson for first-timers. A unique way to explore waterways and enjoy the scenery. 70 | 71 | Geocaching: Best in clear skies, few clouds, or scattered clouds. Perfect for adventurers, families, or groups. Weather conditions affect accessibility and comfort during the hunt. Requires a GPS device or smartphone app. Offers an exciting and educational outdoor treasure hunt experience. 72 | 73 | Botanical Garden Visits: Suitable in clear skies, few clouds, or light rain. Ideal for nature lovers, photographers, or families. Mild weather enhances the beauty and comfort of exploring gardens. A peaceful way to appreciate plant diversity and landscaping artistry. 74 | 75 | Archery: Best in clear skies or few clouds. Suitable for sports enthusiasts or those seeking a new hobby. Weather conditions affect accuracy and safety. Requires access to archery equipment and possibly instruction. Offers a focus on skill development and concentration. 76 | 77 | Horseback Riding: Ideal in clear skies, few clouds, or scattered clouds. Perfect for animal lovers or adventurers. Weather influences trail conditions and comfort for both rider and horse. Requires access to horses and knowledge of riding, often through a stable or equestrian center. A unique way to explore natural settings and bond with animals. 78 | 79 | Street Photography: Best in clear skies or overcast weather. Suitable for photographers, artists, or urban explorers. Weather conditions affect lighting and ambiance in urban settings. Requires a camera and a keen eye for capturing candid moments and urban landscapes. 80 | 81 | Urban Exploration/Walking Tours: Ideal in clear skies, few clouds, or scattered clouds. Perfect for history buffs, culture enthusiasts, or tourists. The weather can enhance the experience of exploring city streets, historical landmarks, and hidden urban gems. Comfortable walking shoes and a guided tour map or app are recommended for an enriching exploration. 82 | 83 | Golf: Best under clear skies or few clouds. Suitable for golfers of all levels, from beginners to experts. Weather conditions greatly influence the playing experience, with clear days providing ideal visibility and course conditions. Requires access to a golf course and equipment. Golf offers a blend of skill, strategy, and outdoor enjoyment. 84 | 85 | Wine Tasting (Outdoor Venues): Enjoyable in clear skies, few clouds, or scattered clouds. Ideal for wine connoisseurs, couples, or groups of friends. The weather adds to the ambiance of outdoor vineyard settings, enhancing the sensory experience of tasting different wines. Knowledgeable guides often lead these tastings, providing insights into wine production and varieties. 86 | 87 | Flea Market Shopping: Best in clear skies or few clouds. Perfect for bargain hunters, antique lovers, or those looking for unique finds. Outdoor flea markets are weather-dependent, with clear days offering a more pleasant browsing experience. Offers a fun and eclectic shopping adventure. 88 | 89 | Kite Flying: Ideal under clear skies with few clouds, particularly enjoyable for families with children or kite enthusiasts. The weather, especially wind conditions, is crucial for successful kite flying. Open fields or beaches are perfect locations, providing ample space and minimal obstructions. 90 | 91 | Sandcastle Building: Best on clear, sunny days at the beach. A fun and creative activity for families, children, or beachgoers. Weather conditions, especially the type of sand and its moisture content, influence the quality of sandcastle construction. Requires simple tools like buckets and shovels, and a dose of creativity. 92 | 93 | Star Party (Astronomy Club Events): Requires clear skies for optimal viewing of celestial bodies. Ideal for astronomy enthusiasts or curious individuals. Weather conditions greatly impact the visibility of stars and planets. Usually organized by astronomy clubs, these events provide telescopes and expert guidance for stargazing. 94 | 95 | Outdoor Sculpture/Art Viewing: Best in clear skies or few clouds. Suitable for art enthusiasts, tourists, or anyone interested in cultural experiences. The weather can enhance the enjoyment and visibility of outdoor art installations. Often found in parks or public spaces, these viewings offer a combination of artistic appreciation and outdoor leisure. 96 | 97 | Camping: Ideal in clear skies, with few clouds. Perfect for nature lovers, adventure seekers, or families. Weather conditions significantly affect the camping experience, with clear weather providing a safer and more enjoyable environment. Requires camping gear and preparation for outdoor living. 98 | 99 | Drive-In Movie Theater: Enjoyable under clear skies or few clouds, ideal for movie enthusiasts, couples, or families. The weather plays a significant role in the comfort and visibility of outdoor screenings. Offers a nostalgic and unique way to watch movies from the comfort of your car. 100 | 101 | Outdoor Concerts/Festivals: Best in clear skies or few clouds. Suitable for music lovers, friends, or families. Weather conditions greatly influence the enjoyment of these events. Outdoor concerts and festivals offer a lively atmosphere and the opportunity to enjoy live performances in a communal setting. 102 | 103 | Bird Feeding/Wildlife Watching: Ideal in clear skies, few clouds, or scattered clouds. Perfect for nature enthusiasts, bird watchers, or families. Weather conditions can affect animal behavior and visibility. Provides a peaceful way to connect with nature and observe local wildlife. 104 | 105 | Metal Detecting: Best in clear skies or few clouds. Suitable for hobbyists, history enthusiasts, or treasure seekers. The weather can influence ground conditions, affecting the ease of metal detecting. Requires a metal detector and possibly research on potential locations. Offers a sense of adventure and the possibility of finding hidden treasures. 106 | 107 | Photography Workshops (Outdoor): Suitable in clear skies, few clouds, scattered clouds, or overcast conditions. Ideal for budding photographers or groups. Weather conditions affect lighting and subject matter, providing varied learning experiences. Workshops often include professional guidance and hands-on practice in different outdoor settings. 108 | 109 | Outdoor Painting/Art Creation: Best in clear skies, few clouds, or scattered clouds. Perfect for artists, hobbyists, or creative individuals. Weather influences light and ambiance, which are crucial for outdoor art creation. Requires art supplies and often a portable easel. Provides a tranquil and inspiring environment for artistic expression. 110 | Frisbee/Golf Disc: Ideal in clear skies or few clouds, perfect for sports enthusiasts, friends, or families. The weather conditions significantly influence gameplay, with clear days providing optimal visibility and less wind interference. This activity offers a combination of casual fun and physical exercise, requiring minimal equipment like a frisbee or golf discs. 111 | 112 | Parasailing: Best in clear skies and mild wind conditions. Suitable for adventure seekers or those looking for a unique beach activity. The weather, especially wind strength, is crucial for a safe and enjoyable experience. Requires a boat and specialized parasailing gear, often provided by tour operators. Offers breathtaking views and an exhilarating sense of flight. 113 | 114 | Snorkeling: Ideal in clear skies and calm waters. Perfect for marine life enthusiasts, swimmers, or tourists in coastal regions. Weather conditions, particularly water clarity, greatly impact the snorkeling experience. Requires snorkeling gear and is often more enjoyable in areas with rich underwater biodiversity. 115 | 116 | Outdoor Chess/Park Games: Best in clear skies, few clouds, or scattered clouds. Suitable for players of all ages, from enthusiasts to casual participants. The weather plays a role in comfort and enjoyment during the game. Outdoor games like chess in parks provide a relaxed yet mentally stimulating environment, often requiring minimal equipment. 117 | 118 | Tree Climbing: Enjoyable in clear skies or few clouds, perfect for nature lovers, adventurers, or children. Weather conditions affect the safety and feasibility of climbing, with dry weather providing better grip and less slippery conditions. Tree climbing can be a playful exploration or a more serious sport, requiring awareness of tree health and safety precautions. 119 | 120 | Scuba Diving: Best in clear skies and calm sea conditions. Ideal for marine enthusiasts, divers, or those seeking an underwater adventure. Weather and sea conditions significantly impact visibility and safety underwater. Requires diving certification, gear, and often the guidance of a dive shop or tour operator. Offers a unique perspective on marine life and underwater landscapes. 121 | 122 | Skydiving: Preferable in clear skies with minimal wind. Suitable for thrill-seekers or those looking for an extreme adventure. Weather conditions are crucial for safety and visibility during the jump. Requires training, safety gear, and usually a tandem jump with an experienced instructor. Offers an adrenaline rush and a unique aerial view. 123 | 124 | Hot Air Ballooning: Ideal in clear skies and stable wind conditions. Perfect for those seeking a serene and scenic experience. Weather plays a vital role in flight safety and route. Requires a balloon, pilot, and often a booked tour. Provides breathtaking views and a peaceful way to observe landscapes from above. 125 | 126 | Ziplining: Best in clear skies or few clouds. Suitable for adventure seekers or families looking for an exciting activity. Weather conditions influence the safety and comfort of the experience. Requires access to a zipline course and safety equipment, often provided at adventure parks or natural reserves. Offers a fast-paced and exhilarating way to explore natural settings. 127 | 128 | Bungee Jumping: Preferable in clear skies with mild wind. Ideal for adrenaline junkies or those looking to challenge their fears. Weather conditions, especially wind, affect the safety and experience of the jump. Requires access to a bungee site and adherence to safety guidelines. Provides an intense thrill and a sense of accomplishment. 129 | 130 | Hang Gliding: Best in clear skies and appropriate wind conditions. Suitable for adventure enthusiasts or those interested in flying sports. Weather significantly impacts flight safety and duration. Requires training, a hang glider, and often assistance from a professional instructor. Offers a serene and exhilarating experience of gliding through the air. 131 | 132 | Street Art Tours: Ideal in clear skies, few clouds, or scattered clouds. Perfect for art enthusiasts, tourists, or urban explorers. Weather conditions affect the comfort of walking tours. These tours offer insights into the urban culture and creativity, showcasing street art, murals, and graffiti in various neighborhoods. 133 | 134 | Historical Landmark Visits: Best in clear skies, few clouds, or scattered clouds. Suitable for history buffs, tourists, or educational trips. Weather conditions can enhance the experience of exploring outdoor historical sites. Offers a glimpse into the past and an educational experience, often with guided tours available. 135 | 136 | Nature Photography Classes: Suitable in a variety of weather conditions including clear skies, few clouds, scattered clouds, or overcast. Ideal for budding photographers or nature lovers. Weather conditions provide diverse lighting and subject matter, enriching the learning experience. These classes often include professional instruction and practical fieldwork. 137 | 138 | Herb Gardening/Planting: Preferable in overcast clouds or light rain. Great for gardening enthusiasts, beginners, or families. Mild weather conditions are ideal for planting and tending to herbs. Offers a rewarding experience of growing your own culinary or medicinal herbs, requiring basic gardening tools and knowledge. 139 | 140 | Outdoor Tai Chi Classes: Best in clear skies, few clouds, or scattered clouds. Ideal for individuals seeking a calming, meditative exercise in nature. Weather conditions enhance the peaceful experience of Tai Chi, providing a serene backdrop. Suitable for all ages and fitness levels, these classes often require minimal equipment, focusing on fluid movements and breathing. 141 | 142 | Butterfly Watching: Perfect in clear skies and mild weather. Suitable for nature enthusiasts, families, or photographers. The weather plays a significant role in butterfly activity, with sunny days often bringing more butterflies out. Requires patience and a gentle approach. Often done in gardens or nature reserves where butterflies are abundant. 143 | 144 | Outdoor Crafting Workshops: Enjoyable in clear skies, few clouds, or scattered clouds. Ideal for crafters, hobbyists, or groups looking for a creative outdoor activity. The weather influences the comfort of crafting outdoors. Workshops might include activities like pottery, woodworking, or painting, often led by experienced instructors. 145 | 146 | Wildflower Identification Walks: Best under clear skies, few clouds, or scattered clouds. Perfect for nature lovers, hikers, or botany enthusiasts. The weather conditions can make walks more pleasant and influence the blooming of different wildflowers. These walks are often educational, providing insights into local flora and ecosystems. 147 | 148 | Nature Journaling: Suitable in clear skies, few clouds, or scattered clouds. Ideal for writers, artists, or nature observers. Weather conditions contribute to the ambiance and inspiration for journaling. This activity encourages mindfulness and a deeper connection with the natural surroundings. 149 | 150 | Bird Nest Building Workshops: Enjoyable in clear skies, few clouds, or scattered clouds. Perfect for families, bird watchers, or nature enthusiasts. Weather conditions affect the comfort of working outdoors. These workshops teach about bird habitat and conservation, providing a hands-on experience in building nests for local bird species. 151 | 152 | Outdoor Cooking Classes: Best in clear skies or few clouds. Suitable for culinary enthusiasts or groups interested in learning new cooking skills. The weather plays a role in the setting and ambiance of outdoor cooking. Classes may cover a range of cuisines and techniques, often involving grills or open fires. 153 | 154 | Sunrise/Sunset Watching: Ideal in clear skies, offering a peaceful and visually stunning experience for photographers, couples, or anyone appreciating natural beauty. The weather greatly influences the visibility and color of the sunrise or sunset. Often enjoyed in scenic locations like beaches, mountains, or open fields. 155 | 156 | Mountain Climbing: Best under clear skies with few clouds. Suitable for adventure seekers, climbers, or outdoor enthusiasts. Weather conditions are critical for safety and visibility during the climb. Requires proper gear, physical fitness, and often the guidance of an experienced climber. Offers a challenging and rewarding experience with spectacular views. 157 | 158 | Lighthouse Visits: Enjoyable in clear skies or few clouds. Ideal for tourists, photographers, or history enthusiasts. Weather conditions influence the ease of access and the views from the lighthouse. Visiting lighthouses often involves learning about maritime history and enjoying coastal scenery. 159 | --------------------------------------------------------------------------------