├── requirements.txt ├── vapi ├── first_message.txt ├── summary_prompt.txt ├── structured_data_prompt.txt ├── structured_data_schema.json ├── assistant_config.json ├── success_evaluation_prompt.txt └── system_prompt.txt ├── LICENSE.md ├── README.md ├── install.py └── google-apps-scripts └── end-of-call-report-logger.gs /requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.7 2 | requests==2.32.3 3 | -------------------------------------------------------------------------------- /vapi/first_message.txt: -------------------------------------------------------------------------------- 1 | Hi, I'm David, an AI assistant with John George VoiceAI Solutions. How can I help you today? -------------------------------------------------------------------------------- /vapi/summary_prompt.txt: -------------------------------------------------------------------------------- 1 | Summarize this AI-led sales qualification call in 100-150 words. Focus on: 2 | 3 | 1. Customer's main pain points or needs 4 | 2. Key product features/benefits discussed 5 | 3. Customer's level of interest and any objections raised 6 | 4. Next steps or actions agreed upon 7 | 5. Overall potential for conversion 8 | 9 | Exclude pleasantries and small talk. Prioritize information crucial for sales follow-up. Use concise, factual language without personal opinions. Output the summary directly, with no introductory text or explanation. If there is not enough information to make a summary, use null. -------------------------------------------------------------------------------- /vapi/structured_data_prompt.txt: -------------------------------------------------------------------------------- 1 | You are an expert data extractor. Extract structured data from the given call transcript according to the JSON Schema below. Pay close attention to the following details: 2 | 3 | 1. Caller's full name: Extract as stated or pieced together from first and last name. 4 | 2. Use case: Identify the specific purpose for the voice AI assistant. 5 | 3. Timescale: Extract the timescale for completion of the project mentioned by the caller, if any. If they say as soon as possible, use "ASAP". 6 | 4. Budget: Extract the indicated budget, whether as a range or specific amount. 7 | 5. Discovery preference: Determine if the caller chose to set up a discovery video conference or send an email. Extract the specific preference as either "video conference" or "email". If the caller didn't make a clear choice or declined both options, use null. 8 | 9 | Extract only the explicitly stated or clearly implied information. If any field is uncertain or not provided, use null. -------------------------------------------------------------------------------- /vapi/structured_data_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "Budget": { 5 | "description": "Extract the indicated budget, whether as a range or specific amount.", 6 | "type": "string" 7 | }, 8 | "Use case": { 9 | "description": "Identify the specific purpose for the voice AI assistant", 10 | "type": "string" 11 | }, 12 | "Full name": { 13 | "description": "Extract as stated or pieced together from first and last name.", 14 | "type": "string" 15 | }, 16 | "Timescale": { 17 | "description": "Extract the timescale for delivery of the voice AI assistant.", 18 | "type": "string" 19 | }, 20 | "Discovery preference": { 21 | "description": "Determine if the caller chose to set up a discovery video conference or send an email. Extract the specific preference as either \"video conference\" or \"email\". If the caller didn't make a clear choice or declined both options, use null.", 22 | "type": "string" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 John George 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 | -------------------------------------------------------------------------------- /vapi/assistant_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AskJG Lead Qualifier Template", 3 | "voice": { 4 | "voiceId": "JBFqnCBsd6RMkjVDRZzb", 5 | "provider": "11labs", 6 | "stability": 0.6, 7 | "similarityBoost": 0.75, 8 | "fillerInjectionEnabled": false, 9 | "optimizeStreamingLatency": 4 10 | }, 11 | "model": { 12 | "model": "gpt-4o", 13 | "messages": [ 14 | { 15 | "role": "system", 16 | "content": "file:///system_prompt.txt" 17 | } 18 | ], 19 | "provider": "openai", 20 | "temperature": 0.1 21 | }, 22 | "firstMessage": "file:///first_message.txt", 23 | "endCallFunctionEnabled": true, 24 | "endCallMessage": "", 25 | "transcriber": { 26 | "model": "nova-2", 27 | "language": "en", 28 | "provider": "deepgram" 29 | }, 30 | "silenceTimeoutSeconds": 26, 31 | "clientMessages": ["tool-calls"], 32 | "serverMessages": ["end-of-call-report"], 33 | "responseDelaySeconds": 0.7, 34 | "llmRequestDelaySeconds": 0, 35 | "maxDurationSeconds": 300, 36 | "backchannelingEnabled": false, 37 | "analysisPlan": { 38 | "summaryPrompt": "file:///summary_prompt.txt", 39 | "structuredDataPrompt": "file:///structured_data_prompt.txt", 40 | "structuredDataSchema": "file:///structured_data_schema.json", 41 | "successEvaluationPrompt": "file:///success_evaluation_prompt.txt", 42 | "successEvaluationRubric": "PassFail" 43 | }, 44 | "backgroundDenoisingEnabled": true, 45 | "messagePlan": { 46 | "idleMessageMaxSpokenCount": 4, 47 | "idleTimeoutSeconds": 6 48 | }, 49 | "startSpeakingPlan": { 50 | "smartEndpointingEnabled": true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /vapi/success_evaluation_prompt.txt: -------------------------------------------------------------------------------- 1 | You are tasked with evaluating the success of an AI-led sales qualification call. Your evaluation will be used to populate the 'success' field in the end-of-call report JSON. 2 | 3 | Input: You will receive the full transcript of the call and any structured data extracted from it. 4 | 5 | Output: Provide a single word response: either "pass" or "fail". 6 | 7 | Evaluation Criteria: 8 | 9 | 1. Information Gathering: 10 | - The AI assistant successfully obtained the caller's full name 11 | - A specific use case for the voice AI assistant was identified 12 | - The caller's budget range was determined 13 | 14 | 2. AI Interaction Quality: 15 | - The caller provided positive feedback about the quality of the AI interaction in terms of speed, accuracy, and naturalness 16 | 17 | 3. Next Steps: 18 | - For a passing call, the caller should have either: 19 | a) Agreed to set up a video conference for a discovery call, or 20 | b) Chosen to send an email for further communication 21 | 22 | 4. Call Navigation: 23 | - The AI assistant successfully navigated to the appropriate page based on the caller's preference (either "book" for video conference or "contact" for email) 24 | 25 | 5. Call Completion: 26 | - The call reached a natural conclusion with the AI assistant properly closing the conversation 27 | 28 | Reasons for Failure: 29 | - The AI assistant failed to gather all required information (name, use case, budget) 30 | - The caller expressed significant dissatisfaction with the AI interaction 31 | - The caller declined both the video conference and email options 32 | - The AI assistant failed to navigate to the appropriate page 33 | - The call ended abruptly or inappropriately 34 | - Any ethical concerns or potential misuse of the technology were raised 35 | 36 | Your task is to carefully analyze the call transcript and provide a "pass" or "fail" evaluation based on the given criteria. Do not include any additional explanation or commentary. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voice AI Solutions Lead Qualification Template 2 | 3 | This repository contains the necessary files to set up a Vapi lead qualfication assistant template for voice AI solutions and a Google Apps Script tool for logging call data. 4 | 5 | ## Setting up the Vapi Assistant 6 | 7 | 1. Clone this repository to your local machine. 8 | 9 | ``` 10 | git clone https://github.com/askjohngeorge/askjg-demo-vapi.git 11 | ``` 12 | 13 | 2. Make sure you have Python 3.7+ installed on your system. 14 | 15 | 3. Install the required dependencies: 16 | 17 | ``` 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | 4. Run the installation script: 22 | 23 | ``` 24 | python install.py 25 | ``` 26 | 27 | 5. When prompted, enter your Vapi private API key. You can obtain this from your Vapi account dashboard. 28 | 29 | 6. The script will create the assistant using the components in the `vapi` directory. 30 | 31 | ## Google Apps Script Tool 32 | 33 | The Google Apps Script tool performs two main functions: 34 | 35 | 1. Logs extracted call data to a Google Sheet 36 | 2. Sends you a formatted email with call details 37 | 38 | ### Setting up the Google Apps Script 39 | 40 | 1. Go to [Google Apps Script](https://script.google.com/) 41 | 42 | 2. Create a new project 43 | 44 | 3. Copy the contents of `end-of-call-report-logger.js` into the script editor 45 | 46 | 4. Replace the placeholder values in the script: 47 | - `SPREADSHEET_ID`: The ID of your Google Sheet (found in the URL) 48 | - `RECIPIENT_EMAIL`: The email address to receive call summaries 49 | 50 | 5. Deploy the script as a web app: 51 | - Click on "Deploy" > "New deployment" 52 | - Choose "Web app" as the type 53 | - Set "Execute as" to your account 54 | - Set "Who has access" to "Anyone" 55 | - Click "Deploy" 56 | 57 | 6. Copy the web app URL provided after deployment 58 | 59 | 7. Update your Vapi assistant configuration to use this URL as the webhook for call data 60 | 61 | ## Usage 62 | 63 | Once set up, the Vapi assistant will handle calls and extract relevant data. After each call, the Google Apps Script will automatically log the data to your specified Google Sheet and send a summary email to the recipient email address. 64 | 65 | ## License 66 | 67 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 68 | 69 | ## Disclaimer 70 | 71 | This project is not officially affiliated with, authorized, maintained, sponsored or endorsed by Vapi or any of its affiliates or subsidiaries. This is an independent and unofficial software. Use at your own risk. -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import requests 4 | import click 5 | 6 | 7 | def read_file(file_path): 8 | with open(file_path, "r", encoding="utf-8") as f: 9 | return f.read() 10 | 11 | 12 | def resolve_file_path(file_reference, directory): 13 | if isinstance(file_reference, str) and file_reference.startswith("file:///"): 14 | return os.path.join(directory, file_reference[8:]) 15 | return file_reference 16 | 17 | 18 | def read_file_if_exists(file_path): 19 | if os.path.exists(file_path): 20 | return read_file(file_path) 21 | return None 22 | 23 | 24 | def recompose_assistant(directory): 25 | config_path = os.path.join(directory, "assistant_config.json") 26 | 27 | if not os.path.exists(config_path): 28 | raise FileNotFoundError(f"assistant_config.json not found in {directory}") 29 | 30 | with open(config_path, "r", encoding="utf-8") as f: 31 | data = json.load(f) 32 | 33 | # Recompose system prompt 34 | system_message = next( 35 | (msg for msg in data["model"]["messages"] if msg["role"] == "system"), None 36 | ) 37 | if system_message: 38 | system_message["content"] = ( 39 | read_file_if_exists(resolve_file_path(system_message["content"], directory)) 40 | or system_message["content"] 41 | ) 42 | 43 | # Recompose firstMessage 44 | if "firstMessage" in data: 45 | data["firstMessage"] = ( 46 | read_file_if_exists(resolve_file_path(data["firstMessage"], directory)) 47 | or data["firstMessage"] 48 | ) 49 | 50 | # Recompose analysisPlan components 51 | if "analysisPlan" in data: 52 | analysis_plan = data["analysisPlan"] 53 | 54 | for key in ["summaryPrompt", "structuredDataPrompt", "successEvaluationPrompt"]: 55 | if key in analysis_plan: 56 | analysis_plan[key] = ( 57 | read_file_if_exists( 58 | resolve_file_path(analysis_plan[key], directory) 59 | ) 60 | or analysis_plan[key] 61 | ) 62 | 63 | if "structuredDataSchema" in analysis_plan: 64 | schema_path = resolve_file_path( 65 | analysis_plan["structuredDataSchema"], directory 66 | ) 67 | if os.path.exists(schema_path): 68 | with open(schema_path, "r", encoding="utf-8") as f: 69 | analysis_plan["structuredDataSchema"] = json.load(f) 70 | 71 | return data 72 | 73 | 74 | def create_assistant(assistant_data, api_key): 75 | url = "https://api.vapi.ai/assistant" 76 | headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} 77 | 78 | try: 79 | response = requests.post(url, headers=headers, json=assistant_data) 80 | response.raise_for_status() 81 | return response.json() 82 | except requests.exceptions.RequestException as e: 83 | print(f"Error creating assistant: {e}") 84 | if e.response: 85 | print(f"Response details: {e.response.text}") 86 | raise SystemExit(1) 87 | 88 | 89 | @click.command() 90 | @click.option("--api-key", prompt=True, hide_input=True, help="Your Vapi API key") 91 | @click.option( 92 | "--directory", default="vapi", help="Directory containing assistant components" 93 | ) 94 | def main(api_key, directory): 95 | """ 96 | Recompose and create a Vapi assistant from components in the specified directory. 97 | """ 98 | try: 99 | assistant_data = recompose_assistant(directory) 100 | created_assistant = create_assistant(assistant_data, api_key) 101 | print( 102 | f"Assistant created successfully. Assistant ID: {created_assistant['id']}" 103 | ) 104 | except FileNotFoundError as e: 105 | print(f"Error: {e}") 106 | raise SystemExit(1) 107 | 108 | 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /vapi/system_prompt.txt: -------------------------------------------------------------------------------- 1 | # [Identity] 2 | You are David, a helpful voice assistant for John George Voice AI Solutions. You are accessible via a widget on the website. You take pride in customer satisfaction and maintaining a friendly, professional demeanor throughout your interactions. 3 | 4 | # [Style] 5 | - You are currently operating as a voice conversation, so use natural language and be concise. 6 | - Maintain a warm, professional, and polite tone. 7 | - After asking a question, wait for the caller to respond before moving to the next question. Never ask more than one question at a time. 8 | - Do not go off-topic, ask, or answer any questions that are not related to the tasks. 9 | - If you perfectly follow your instructions, you will be rewarded with a bonus. 10 | 11 | # [Tasks] 12 | 1. Build rapport and establish caller's name 13 | - Engage in a friendly discussion based on the caller's initial response to "How can I help you?" 14 | - Show genuine interest in their situation and respond appropriately to build a connection. 15 | - Smoothly transition to asking for the caller's full name if not already provided, e.g., "I'll be happy to assist you with that. So that I address you correctly, may I know your full name?" 16 | - Use their name naturally in the following interactions to maintain a personal touch. Establishing their name is crucial for the following steps. Only continue if they provide their name or if they explicitly decline to provide it. 17 | 18 | 2. Identify specific use case. 19 | - Ask open-ended questions to determine the precise purpose of the AI assistant. 20 | - e.g.: "So, [Name], what kind of voice AI solution are you looking for? Could you tell me a bit about your needs?" 21 | - Aim for a focused, singular use case like customer service, sales outreach, appointment bookings, automated reminders, lead qualification, smart voicemail, conducting surveys, or handling FAQs. 22 | - Follow up to refine broad answers into more specific applications. 23 | 24 | 3. Establish project timescales. 25 | - Ask about the desired start date, e.g., "When are you hoping to implement this voice AI solution?" 26 | - Inquire about the project deadline, e.g., "Do you have a target date in mind for when you'd like the solution to be fully operational?" 27 | 28 | 4. Determine budget range. 29 | - Begin with an open-ended question: "Have you considered a budget for this voice AI solution?" 30 | - Only if the caller is unsure or asks for guidance, offer tiered options: "To give you an idea, initial setups typically fall into ranges like two to five thousand pounds, five to ten thousand pounds, or more than ten thousand pounds. Does one of these align with your expectations?" 31 | - Clarify that the budget is for setup only, and there are ongoing costs associated with the service. 32 | - If asked about ongoing costs, explain that these depend on the complexity of their use case and would need to be discussed further with John George. 33 | 34 | 5. Assess AI interaction experience. 35 | - e.g. "[Name], I'm curious about your experience with our conversation so far. How do you feel about the quality of our interaction in terms of responsiveness, understanding, and naturalness?" 36 | 37 | 6. Offer discovery call options. 38 | - e.g. "Would you be interested in exploring your needs in more detail with our founder, John George? We can arrange a video conference for a discovery call, or if you prefer, you can send an email with your questions. Which option would you find more convenient?" 39 | - If the caller expresses interest in either option, proceed to step 7 to navigate to the appropriate page. 40 | - If the caller is not interested in either option, thank them warmly for their interest, and then end the call by calling the endCall function. 41 | 42 | 7. Navigate to the appropriate page based on the caller's preference. 43 | - Use the navigate-askjg tool to navigate to "book" if the caller prefers a video conference, or "contact" if they prefer to send an email. 44 | - Once you've navigated to the appropriate page, inform the caller that they have been redirected. 45 | - e.g. "I've directed you to our booking page where you can set up your video conference with John. Is there anything else I can help you with?" 46 | - e.g. "You're now on our contact page where you can send your email inquiry. Is there any other information you need?" 47 | 48 | 8. Close call. 49 | - e.g. "Thank you so much for your interest in our services, [Name]. We're looking forward to helping you create an excellent voice AI solution. Have a great day!" 50 | - End the call by calling the endCall function. -------------------------------------------------------------------------------- /google-apps-scripts/end-of-call-report-logger.gs: -------------------------------------------------------------------------------- 1 | // Configuration 2 | const CONFIG = { 3 | EMAIL_ADDRESS: "your.email@example.com", 4 | SPREADSHEET_ID: "your_spreadsheet_id_here", 5 | SHEET_NAME: "Call_Logs", 6 | }; 7 | 8 | function doPost(e) { 9 | const jsonData = e.postData.contents; 10 | 11 | // Process the data 12 | const processedData = processJsonData(jsonData); 13 | 14 | // Send email 15 | sendFormattedEmail(processedData); 16 | 17 | // Log to spreadsheet 18 | logToSpreadsheet(processedData); 19 | 20 | return ContentService.createTextOutput("Success"); 21 | } 22 | 23 | function processJsonData(jsonData) { 24 | const parsedData = JSON.parse(jsonData); 25 | const timestamp = new Date(); 26 | 27 | return { 28 | timestamp: timestamp, 29 | rawJson: jsonData, 30 | callId: parsedData.message.call.id, 31 | endReason: parsedData.message.endedReason, 32 | durationSeconds: parsedData.message.durationSeconds, 33 | durationMinutes: parsedData.message.durationMinutes, 34 | fullName: parsedData.message.analysis.structuredData["Full name"], 35 | useCase: parsedData.message.analysis.structuredData["Use case"], 36 | timescale: parsedData.message.analysis.structuredData.Timescale, 37 | budget: parsedData.message.analysis.structuredData.Budget, 38 | discoveryPreference: 39 | parsedData.message.analysis.structuredData["Discovery preference"], 40 | callSummary: parsedData.message.analysis.summary, 41 | formattedTranscript: createFormattedTranscript(parsedData.message.messages), 42 | monoRecordingUrl: parsedData.message.recordingUrl, 43 | stereoRecordingUrl: parsedData.message.stereoRecordingUrl, 44 | }; 45 | } 46 | 47 | function sendFormattedEmail(data) { 48 | const subject = `AskJG VapiWidget Call Report: ${ 49 | data.fullName ?? "Unknown Caller" 50 | } ${data.useCase ? " - " + data.useCase : ""}`; 51 | const body = createEmailBody(data); 52 | 53 | MailApp.sendEmail({ 54 | to: CONFIG.EMAIL_ADDRESS, 55 | subject: subject, 56 | htmlBody: body, 57 | }); 58 | } 59 | 60 | function createEmailBody(data) { 61 | return ` 62 | 63 |
64 | 70 | 71 | 72 || Timestamp | ${data.timestamp} |
|---|---|
| Call ID | ${data.callId} |
| End Reason | ${data.endReason} |
| Duration | ${data.durationMinutes.toFixed( 78 | 2 79 | )} minutes (${data.durationSeconds} seconds) |
| Full Name | ${ 81 | data.fullName ?? "Unknown Caller" 82 | } |
| Use Case | ${data.useCase ?? "N/A"} |
| Timescale | ${data.timescale ?? "N/A"} |
| Budget | ${data.budget ?? "N/A"} |
| Discovery Preference | ${ 87 | data.discoveryPreference ?? "N/A" 88 | } |
| Call Summary | ${ 90 | data.callSummary !== "null" ? data.callSummary : "N/A" 91 | } |
| Formatted Transcript | ${
93 | data.formattedTranscript
94 | } |
| Mono Recording URL | Listen |
| Stereo Recording URL | Listen |