Spaces:
Runtime error
Runtime error
| 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 --- | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| 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 | |
| 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 |