sonuprasad23 commited on
Commit
882dc25
·
1 Parent(s): 482c410
Files changed (4) hide show
  1. Dockerfile +7 -8
  2. app.py +179 -0
  3. requirements.txt +5 -2
  4. webhook_server.py +0 -86
Dockerfile CHANGED
@@ -1,19 +1,18 @@
1
- # Use a slim, official Python image
2
  FROM python:3.10-slim
3
 
4
- # Set the working directory inside the container
5
  WORKDIR /app
6
 
7
- # Copy the requirements file and install dependencies
8
- # This leverages Docker caching to speed up future builds
9
  COPY requirements.txt .
10
  RUN pip install --no-cache-dir -r requirements.txt
11
 
12
- # Copy the rest of your application files into the container
13
  COPY . .
14
 
15
- # Expose the port that Hugging Face uses
16
  EXPOSE 7860
17
 
18
- # Command to run the application using a production-grade server
19
- CMD ["gunicorn", "--bind", "0.0.0.0:7860", "webhook_server:app"]
 
1
+ # Use a stable, official Python image
2
  FROM python:3.10-slim
3
 
4
+ # Set the working directory
5
  WORKDIR /app
6
 
7
+ # Copy requirements and install them to leverage Docker caching
 
8
  COPY requirements.txt .
9
  RUN pip install --no-cache-dir -r requirements.txt
10
 
11
+ # Copy the rest of the application files
12
  COPY . .
13
 
14
+ # Expose the port Hugging Face uses for Streamlit apps
15
  EXPOSE 7860
16
 
17
+ # Command to run the unified application
18
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
app.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import threading
4
+ import streamlit as st
5
+ import gspread
6
+ import google.generativeai as genai
7
+ from flask import Flask, request, jsonify
8
+ from oauth2client.service_account import ServiceAccountCredentials
9
+ from vapi import Vapi
10
+ from datetime import datetime
11
+
12
+ # --- Configuration Loading ---
13
+ def load_and_set_config():
14
+ """Loads configuration from a single Hugging Face secret (JSON) and sets them as environment variables."""
15
+ config_json_str = os.getenv("APP_CONFIG_JSON")
16
+ if not config_json_str:
17
+ st.error("FATAL: The 'APP_CONFIG_JSON' secret is not set in your Space settings!")
18
+ st.stop()
19
+
20
+ config = json.loads(config_json_str)
21
+ for key, value in config.items():
22
+ os.environ[key] = str(value)
23
+ return config
24
+
25
+ # --- VAPI Call Logic ---
26
+ def place_vapi_call(config, customer_number, raw_intent, target_name):
27
+ # Process intent to generate a good first message
28
+ genai.configure(api_key=config["GEMINI_API_KEY"])
29
+ model = genai.GenerativeModel('gemini-1.5-flash')
30
+
31
+ prompt_details = f"The user's raw intent is: '{raw_intent}'."
32
+ if target_name:
33
+ prompt_details += f" They are calling a person named '{target_name}'."
34
+
35
+ gemini_prompt = f"""
36
+ Based on the user's intent, generate a single, natural, and polite opening sentence for a phone call.
37
+ {prompt_details}
38
+ If a name is provided, start by asking for them. Example: "Hello, may I please speak with {target_name}?" followed by the reason.
39
+ If no name is provided, use a generic opening. Example: "Hello, I'm calling on behalf of a user to inquire about something."
40
+ Your output should be ONLY the single sentence string.
41
+ """
42
+
43
+ try:
44
+ response = model.generate_content(gemini_prompt)
45
+ first_message = response.text.strip()
46
+ except Exception as e:
47
+ st.error(f"Error with Gemini: {e}")
48
+ return None
49
+
50
+ # Place the actual call
51
+ try:
52
+ vapi = Vapi(token=config["VAPI_API_KEY"])
53
+ system_prompt = "You are a helpful and polite AI assistant named Alex. Your first sentence has already been spoken. You are now in an active conversation. Listen carefully and respond naturally. Continue the conversation in the language the other person is speaking (English or Hindi)."
54
+
55
+ return vapi.calls.create(
56
+ assistant_id=config["VAPI_ASSISTANT_ID"],
57
+ phone_number_id=config["VAPI_PHONE_NUMBER_ID"],
58
+ customer={"number": customer_number},
59
+ assistant={
60
+ "serverUrl": config["WEBHOOK_SERVER_URL"],
61
+ "firstMessage": first_message,
62
+ "voice": {"provider": "11labs", "voiceId": "pola", "speed": 0.9},
63
+ "model": {"provider": "openai", "model": "gpt-4o", "messages": [{"role": "system", "content": system_prompt}]},
64
+ "metadata": {"raw_intent": f"{raw_intent} (for {target_name})"}
65
+ }
66
+ )
67
+ except Exception as e:
68
+ st.error(f"An error occurred placing the Vapi call: {e}")
69
+ return None
70
+
71
+ # --- FLASK WEBHOOK (Runs in the Background) ---
72
+ flask_app = Flask(__name__)
73
+
74
+ def log_to_google_sheet(row_data, config):
75
+ try:
76
+ print("[Log] Attempting to log to Google Sheets...")
77
+ if not os.path.exists("credentials.json"):
78
+ print("[Log] FATAL: 'credentials.json' not found.")
79
+ return
80
+
81
+ 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"])
82
+ client = gspread.authorize(creds)
83
+
84
+ sheet = client.open(config["GOOGLE_SHEET_NAME"]).sheet1
85
+ sheet.append_row(row_data)
86
+ print(f"[Log] Successfully logged call data.")
87
+ except Exception as e:
88
+ print(f"[Log] !!! An error occurred during Google Sheets logging: {e} !!!")
89
+
90
+ @flask_app.route("/webhook", methods=['POST'])
91
+ def webhook_handler():
92
+ print(f"\n--- Webhook received POST request from Vapi at {datetime.now()} ---")
93
+ try:
94
+ payload = request.json
95
+ if payload.get('message', {}).get('type') == 'hang':
96
+ call_data = payload.get('message', {}).get('call', {})
97
+ row_to_insert = [
98
+ call_data.get("id", "N/A"), datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
99
+ call_data.get("customer", {}).get("number", "N/A"), call_data.get("metadata", {}).get("raw_intent", "N/A"),
100
+ payload.get('message', {}).get('endedReason', 'N/A'), call_data.get('summary', 'No summary provided.'),
101
+ "Yes" if "follow-up" in call_data.get('summary', '').lower() else "No",
102
+ json.dumps(call_data.get('transcript', []), indent=2)
103
+ ]
104
+ config = json.loads(os.getenv("APP_CONFIG_JSON"))
105
+ log_to_google_sheet(row_to_insert, config)
106
+ except Exception as e:
107
+ print(f"Error in webhook handler: {e}")
108
+ return jsonify({'status': 'success'}), 200
109
+
110
+ def run_flask():
111
+ # Running on a different port than Streamlit
112
+ flask_app.run(host="0.0.0.0", port=8080)
113
+
114
+ # --- STREAMLIT UI (Main Application) ---
115
+ def run_streamlit():
116
+ st.set_page_config(page_title="Voice AI Coordinator", layout="centered")
117
+
118
+ # Custom CSS for a pleasing look
119
+ st.markdown("""
120
+ <style>
121
+ .stApp {
122
+ background-color: #f0f2f5;
123
+ }
124
+ .stForm {
125
+ background-color: white;
126
+ padding: 2em;
127
+ border-radius: 10px;
128
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
129
+ }
130
+ .stButton>button {
131
+ width: 100%;
132
+ border: none;
133
+ border-radius: 5px;
134
+ padding: 10px;
135
+ font-weight: bold;
136
+ color: white;
137
+ background-color: #ff4b4b;
138
+ }
139
+ </style>
140
+ """, unsafe_allow_html=True)
141
+
142
+ st.title("🚀 Voice AI Coordination Tool")
143
+ st.markdown("A unified interface to deploy intelligent voice agents on your behalf.")
144
+
145
+ # Load configuration
146
+ try:
147
+ config = load_and_set_config()
148
+ except Exception as e:
149
+ st.error(f"Failed to load configuration. Please ensure APP_CONFIG_JSON secret is set correctly. Error: {e}")
150
+ st.stop()
151
+
152
+ with st.form(key="call_form"):
153
+ st.subheader("📞 Place a New Call")
154
+ target_name = st.text_input("Whom are you calling? (Optional)", placeholder="e.g., Jane Doe")
155
+ phone_number = st.text_input("Phone Number (with country code)", placeholder="+911234567890")
156
+ raw_intent = st.text_area("Reason for Calling", placeholder="Ask if they would prefer tea or coffee", height=100)
157
+
158
+ submit_button = st.form_submit_button(label="Deploy Call")
159
+
160
+ if submit_button:
161
+ if not phone_number or not raw_intent:
162
+ st.warning("Please provide a phone number and a reason for the call.")
163
+ else:
164
+ with st.spinner("Processing intent and placing call..."):
165
+ call_response = place_vapi_call(config, phone_number, raw_intent, target_name)
166
+ if call_response and call_response.id:
167
+ st.success(f"✅ Call deployed successfully! Call ID: {call_response.id}")
168
+ st.info("The call is being connected. Once it ends, the data will be logged to your Google Sheet.")
169
+ st.balloons()
170
+ else:
171
+ st.error("❌ Failed to deploy the call. Please check the Space logs for errors.")
172
+
173
+ if __name__ == "__main__":
174
+ # Run Flask in a background daemon thread
175
+ flask_thread = threading.Thread(target=run_flask, daemon=True)
176
+ flask_thread.start()
177
+
178
+ # Run the main Streamlit app
179
+ run_streamlit()
requirements.txt CHANGED
@@ -1,4 +1,7 @@
1
- Flask
 
2
  gspread
3
  oauth2client
4
- gunicorn
 
 
 
1
+ streamlit
2
+ flask
3
  gspread
4
  oauth2client
5
+ vapi-python
6
+ google-generativeai
7
+ python-dotenv
webhook_server.py DELETED
@@ -1,86 +0,0 @@
1
- import os
2
- import json
3
- import gspread
4
- from oauth2client.service_account import ServiceAccountCredentials
5
- from flask import Flask, request, jsonify
6
- from datetime import datetime
7
-
8
- app = Flask(__name__)
9
-
10
- HTML_TEMPLATE = """
11
- <!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Vapi Webhook Server Status</title><style>body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f0f2f5; }} .container {{ text-align: center; padding: 40px; background-color: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }} .status {{ display: inline-block; padding: 12px 24px; background-color: #28a745; color: white; font-size: 20px; font-weight: 600; border-radius: 8px; margin-bottom: 20px; }} h1 {{ color: #333; }} p {{ color: #555; font-size: 16px; }} code {{ background-color: #e9ecef; padding: 4px 8px; border-radius: 4px; font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace; }}</style></head><body><div class="container"><div class="status">Live</div><h1>Your Vapi Webhook Server is Running!</h1><p>This server is successfully deployed and ready to receive requests from Vapi.</p><p>Use the following full URL in your application's configuration:</p><code>{full_webhook_url}</code></div></body></html>
12
- """
13
-
14
- def log_to_google_sheet(row_data):
15
- """A dedicated, robust function to handle Google Sheets logging with extreme verbosity."""
16
- try:
17
- # --- STAGE 1: CHECK FOR CREDENTIALS FILE ---
18
- print("[Log] Stage 1: Checking for 'credentials.json' file...")
19
- if not os.path.exists("credentials.json"):
20
- print("[Log] FATAL ERROR: 'credentials.json' was not found. Have you uploaded it to the Space?")
21
- return
22
- print("[Log] Stage 1 SUCCESS: 'credentials.json' found.")
23
-
24
- # --- STAGE 2: AUTHENTICATE WITH GOOGLE ---
25
- print("[Log] Stage 2: Authenticating with Google using the credentials file...")
26
- scope = ["https://spreadsheets.google.com/feeds", 'https://www.googleapis.com/auth/spreadsheets',
27
- "https://www.googleapis.com/auth/drive.file", "https://www.googleapis.com/auth/drive"]
28
- creds = ServiceAccountCredentials.from_json_keyfile_name("credentials.json", scope)
29
- client = gspread.authorize(creds)
30
- print("[Log] Stage 2 SUCCESS: Google Authentication successful.")
31
-
32
- # --- STAGE 3: CHECK FOR SHEET NAME ---
33
- print("[Log] Stage 3: Checking for GOOGLE_SHEET_NAME secret...")
34
- sheet_name = os.getenv("GOOGLE_SHEET_NAME")
35
- if not sheet_name:
36
- print("[Log] FATAL ERROR: The GOOGLE_SHEET_NAME secret is not set in your Space settings.")
37
- return
38
- print(f"[Log] Stage 3 SUCCESS: Found sheet name secret: '{sheet_name}'.")
39
-
40
- # --- STAGE 4: OPEN THE SHEET ---
41
- print(f"[Log] Stage 4: Attempting to open Google Sheet '{sheet_name}'...")
42
- sheet = client.open(sheet_name).sheet1
43
- print("[Log] Stage 4 SUCCESS: Successfully opened the sheet.")
44
-
45
- # --- STAGE 5: APPEND THE ROW ---
46
- print(f"[Log] Stage 5: Appending row data: {row_data}")
47
- sheet.append_row(row_data)
48
- print("[Log] Stage 5 SUCCESS: Row appended successfully!")
49
-
50
- except Exception as e:
51
- print(f"\n!!!!!!!!!!!!!! [Log] A CRITICAL ERROR OCCURRED DURING GOOGLE SHEETS LOGGING !!!!!!!!!!!!!!")
52
- print(f"ERROR DETAILS: {e}")
53
- print("This is likely due to one of three reasons:")
54
- print("1. The `credentials.json` file is missing or malformed.")
55
- print("2. The GOOGLE_SHEET_NAME secret in your Space does not EXACTLY match your sheet's name.")
56
- print("3. You have not SHARED your Google Sheet with the `client_email` from the credentials file.\n")
57
-
58
- @app.route('/', methods=['GET'])
59
- def root():
60
- host = request.host_url
61
- full_url = f"{host}webhook"
62
- return HTML_TEMPLATE.format(full_webhook_url=full_url)
63
-
64
- @app.route('/webhook', methods=['POST'])
65
- def webhook_handler():
66
- print(f"\n--- Webhook received POST request from Vapi at {datetime.now()} ---")
67
- try:
68
- payload = request.json
69
- if payload.get('message', {}).get('type') == 'hang':
70
- print("Received 'hang' event. Preparing to log call details...")
71
- call_data = payload.get('message', {}).get('call', {})
72
- row_to_insert = [
73
- call_data.get("id", "N/A"), datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
74
- call_data.get("customer", {}).get("number", "N/A"), call_data.get("metadata", {}).get("raw_intent", "N/A"),
75
- payload.get('message', {}).get('endedReason', 'N/A'), call_data.get('summary', 'No summary provided.'),
76
- "Yes" if "follow-up" in call_data.get('summary', '').lower() else "No",
77
- json.dumps(call_data.get('transcript', []), indent=2)
78
- ]
79
- log_to_google_sheet(row_to_insert)
80
- except Exception as e:
81
- print(f"An error occurred in the main webhook handler: {e}")
82
- return jsonify({'status': 'success'}), 200
83
-
84
- if __name__ == "__main__":
85
- port = int(os.environ.get("PORT", 7860))
86
- app.run(host="0.0.0.0", port=port)