faffit / app.py
sonuprasad23's picture
Uploaded
c9868a1
import os
import json
import gspread
import google.generativeai as genai
from flask import Flask, request, jsonify, render_template_string
from oauth2client.service_account import ServiceAccountCredentials
from vapi import Vapi
from datetime import datetime
# --- App Initialization ---
app = Flask(__name__)
# --- Configuration Loading ---
def load_config():
config_json_str = os.getenv("APP_CONFIG_JSON")
if not config_json_str:
raise ValueError("FATAL: The 'APP_CONFIG_JSON' secret is not set in your Space settings!")
return json.loads(config_json_str)
# --- HTML/CSS/JS Front-End Template ---
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voice AI Coordinator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background-color: #f3f4f6; color: #1f2937; }
.container { width: 100%; max-width: 450px; padding: 2rem; background-color: white; border-radius: 12px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); }
.header { text-align: center; margin-bottom: 2rem; }
.header h1 { font-size: 2rem; font-weight: 700; margin: 0; }
.header p { margin-top: 0.5rem; color: #6b7280; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
.form-group input, .form-group textarea { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 8px; box-sizing: border-box; font-size: 1rem; transition: border-color 0.2s, box-shadow 0.2s; }
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); }
.deploy-button { width: 100%; padding: 0.8rem; border: none; border-radius: 8px; background-color: #ef4444; color: white; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: background-color 0.2s; }
.deploy-button:hover { background-color: #dc2626; }
.deploy-button:disabled { background-color: #fca5a5; cursor: not-allowed; }
.status { margin-top: 1.5rem; padding: 0.75rem; border-radius: 8px; text-align: center; font-weight: 500; display: none; }
.status.success { background-color: #d1fae5; color: #065f46; display: block; }
.status.error { background-color: #fee2e2; color: #991b1b; display: block; }
.status.loading { background-color: #e0e7ff; color: #3730a3; display: block; }
</style>
</head>
<body>
<div class="container">
<div class="header"><h1>πŸš€ Voice AI Coordinator</h1><p>Deploy a voice agent to make calls on your behalf.</p></div>
<form id="call-form">
<div class="form-group"><label for="target-name">Whom to call (Name)</label><input type="text" id="target-name" placeholder="e.g., Jane Doe (Optional)"></div>
<div class="form-group"><label for="phone-number">Phone Number</label><input type="tel" id="phone-number" placeholder="+911234567890" required></div>
<div class="form-group"><label for="raw-intent">Reason for Calling</label><textarea id="raw-intent" rows="3" placeholder="e.g., Ask if they would prefer tea or coffee" required></textarea></div>
<button type="submit" id="deploy-button" class="deploy-button">Deploy Call</button>
</form>
<div id="status-message" class="status"></div>
</div>
<script>
const form = document.getElementById('call-form');
const button = document.getElementById('deploy-button');
const statusDiv = document.getElementById('status-message');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const name = document.getElementById('target-name').value;
const number = document.getElementById('phone-number').value;
const reason = document.getElementById('raw-intent').value;
button.disabled = true;
button.textContent = 'Deploying...';
statusDiv.className = 'status loading';
statusDiv.textContent = 'Processing intent and placing call...';
try {
const response = await fetch('/place-call', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, number, reason })
});
const result = await response.json();
if (response.ok) {
statusDiv.className = 'status success';
statusDiv.textContent = `βœ… Success! Call deployed with ID: ${result.call_id}`;
} else { throw new Error(result.error || 'An unknown error occurred.'); }
} catch (error) {
statusDiv.className = 'status error';
statusDiv.textContent = `❌ Error: ${error.message}`;
} finally {
button.disabled = false;
button.textContent = 'Deploy Call';
}
});
</script>
</body>
</html>
"""
# --- Routes ---
@app.route('/', methods=['GET'])
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/place-call', methods=['POST'])
def place_call_handler():
try:
config = load_config()
data = request.get_json()
name, number, reason = data['name'], data['number'], data['reason']
genai.configure(api_key=config["GEMINI_API_KEY"])
model = genai.GenerativeModel('gemini-1.5-flash')
prompt_details = f"User's intent: '{reason}'. Calling: '{name if name else 'N/A'}'."
gemini_prompt = f"Generate a single, polite opening sentence for a phone call. {prompt_details} If a name is provided, ask for them. Output ONLY the sentence string."
response = model.generate_content(gemini_prompt)
first_message = response.text.strip().replace('"', '')
# --- FIX 1: Initialize the Vapi client using the `token` keyword argument. ---
vapi = Vapi(token=config["VAPI_API_KEY"])
system_prompt = "You are Alex, a polite AI assistant. Your first sentence has been spoken. Now, listen and respond naturally in English or Hindi."
# --- FIX 2: Create the call using the `client.calls.create` attribute (plural). ---
call_response = vapi.calls.create(
assistant_id=config["VAPI_ASSISTANT_ID"], phone_number_id=config["VAPI_PHONE_NUMBER_ID"],
customer={"number": number},
assistant={
"serverUrl": config["WEBHOOK_SERVER_URL"], "firstMessage": first_message,
"voice": {"provider": "11labs", "voiceId": "pola", "speed": 0.9},
"model": {"provider": "openai", "model": "gpt-4o", "messages": [{"role": "system", "content": system_prompt}]},
"metadata": {"raw_intent": f"{reason} (for {name})"}
})
if call_response and call_response.id:
return jsonify({"message": "Call deployed successfully!", "call_id": call_response.id}), 200
else:
raise Exception("Vapi did not return a valid call object.")
except Exception as e:
print(f"[ERROR] in /place-call: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/webhook', methods=['POST'])
def webhook_handler():
try:
config = load_config()
payload = request.json
if payload.get('message', {}).get('type') == 'hang':
call_data = payload.get('message', {}).get('call', {})
row = [
call_data.get("id", "N/A"), datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
call_data.get("customer", {}).get("number", "N/A"), call_data.get("metadata", {}).get("raw_intent", "N/A"),
payload.get('message', {}).get('endedReason', 'N/A'), call_data.get('summary', 'No summary provided.'),
"Yes" if "follow-up" in call_data.get('summary', '').lower() else "No",
json.dumps(call_data.get('transcript', []), indent=2)
]
creds = ServiceAccountCredentials.from_json_keyfile_name("credentials.json", ["https://spreadsheets.google.com/feeds", 'https://www.googleapis.com/auth/spreadsheets', "https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive"])
client = gspread.authorize(creds)
sheet = client.open(config["GOOGLE_SHEET_NAME"]).sheet1
sheet.append_row(row)
print(f"[Log] Successfully logged call data for {call_data.get('id')}.")
except Exception as e:
print(f"[ERROR] in /webhook: {e}")
return jsonify({'status': 'success'}), 200