rairo commited on
Commit
93cf855
·
verified ·
1 Parent(s): 7443fd5

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +70 -230
main.py CHANGED
@@ -951,265 +951,105 @@ def delete_project(project_id):
951
  #------------------------
952
  # AI phone call ElevenLabs
953
  #-------------------------
 
954
 
955
- # Configuration
956
- CREDITS_PER_MIN = 3 # Adjust based on ElevenLabs conversational AI pricing
957
- VOICE_NAME = "Daniel" # British male voice (more reliable than Archer)
958
-
959
- # Known ElevenLabs voice IDs for common voices
960
- KNOWN_VOICE_IDS = {
961
- "Daniel": "onwK4e9ZLuTAKqWW03F9", # British male
962
- "Charlie": "IKne3meq5aSn9XLyUdCD", # British male
963
- "George": "JBFqnCBsd6RMkjVDRZzb", # British male
964
- "Archer": "D38z5RcWu1voky8WS1ja", # British male
965
- "Adam": "pNInz6obpgDQGcFmaJgB", # American male (fallback)
966
- "Antoni": "ErXwobaYiN019PkySvjV", # American male (fallback)
967
- }
968
-
969
-
970
- class ConversationalAIHandler:
971
- def __init__(self):
972
- # This is the correct base URL for this specific API
973
- self.base_url = "https://api.elevenlabs.io/v1/convai"
974
- self.api_key = os.getenv("ELEVENLABS_API_KEY")
975
- if not self.api_key:
976
- logger.critical("[CONVAI] ELEVENLABS_API_KEY environment variable not set.")
977
- raise ValueError("ELEVENLABS_API_KEY environment variable not set.")
978
- self.headers = {
979
- "xi-api-key": self.api_key,
980
- "Content-Type": "application/json"
981
- }
982
-
983
- def get_british_male_voice_id(self):
984
- """Gets a known British male voice ID."""
985
- logger.info(f"[CONVAI] Using known voice ID for Daniel.")
986
- return KNOWN_VOICE_IDS.get("Daniel", "onwK4e9ZLuTAKqWW03F9")
987
 
988
- def create_or_get_agent(self, project_id, project_data):
989
- """Creates or retrieves a Convai agent."""
990
- try:
991
- logger.info(f"[CONVAI] Starting agent creation/retrieval for project: {project_id}")
992
 
993
- existing_agent_id = db_ref.child(f'projects/{project_id}/agent_id').get()
994
- if existing_agent_id:
995
- # The Convai API does not have a simple GET /agents/{id} endpoint for verification.
996
- # We will trust the ID from our database.
997
- logger.info(f"[CONVAI] Found existing agent ID in DB: {existing_agent_id}. Assuming active.")
998
- return existing_agent_id
999
 
1000
- logger.info(f"[CONVAI] Creating new Convai agent.")
1001
- voice_id = self.get_british_male_voice_id()
 
 
 
 
 
 
 
 
 
1002
 
1003
- diy_expert_prompt = f"""
1004
- You are an experienced DIY expert... [Your full prompt here] ...
1005
- """
1006
-
1007
- agent_payload = {
1008
- "conversation_config": {
1009
- "name": f"DIY Expert - {project_data.get('projectTitle', project_id)}",
1010
- "voice_id": voice_id,
1011
- "model_id": "eleven_turbo_v2",
1012
- "first_message": "Hello! I'm your Sozo DIY expert assistant. I'm here to help you with your project. What would you like to work on today?",
1013
- "prompt": diy_expert_prompt
1014
- }
1015
- }
1016
- creation_url = f"{self.base_url}/agents/create"
1017
- response = requests.post(creation_url, headers=self.headers, json=agent_payload, timeout=30)
1018
-
1019
- if response.status_code == 200:
1020
- agent_info = response.json()
1021
- agent_id = agent_info.get("agent_id")
1022
- logger.info(f"[CONVAI] New agent created successfully: {agent_id}")
1023
- db_ref.child(f'projects/{project_id}').update({'agent_id': agent_id})
1024
- return agent_id
1025
- else:
1026
- logger.error(f"[CONVAI] Failed to create agent. Status: {response.status_code}, Response: {response.text}")
1027
- raise Exception(f"Failed to create agent: HTTP {response.status_code} - {response.text}")
1028
 
1029
- except Exception as e:
1030
- logger.error(f"[CONVAI] An exception occurred in create_or_get_agent: {e}")
1031
- raise
1032
 
1033
- def get_conversation_url(self, agent_id):
1034
- """
1035
- Gets a signed WebSocket URL to start a conversation.
1036
- This is the only method needed to start the conversation flow.
1037
- """
1038
- try:
1039
- logger.info(f"[CONVAI] Requesting signed WebSocket URL for agent: {agent_id}")
1040
- url = f"{self.base_url}/conversation/get-signed-url"
1041
- params = {"agent_id": agent_id}
1042
-
1043
- resp = requests.get(url, headers=self.headers, params=params, timeout=15)
1044
 
1045
- if resp.status_code != 200:
1046
- logger.error(f"[CONVAI] Signed-URL request failed ({resp.status_code}): {resp.text}")
1047
- raise Exception(f"Could not get signed URL: {resp.text}")
1048
 
1049
- data = resp.json()
1050
- signed_url = data.get("signed_url")
1051
- if not signed_url:
1052
- logger.error("[CONVAI] Missing 'signed_url' in response.")
1053
- raise Exception("Invalid signed URL response from ElevenLabs")
1054
 
1055
- logger.info("[CONVAI] Successfully retrieved WebSocket URL.")
1056
- return signed_url
1057
-
1058
- except Exception as e:
1059
- logger.error(f"[CONVAI] Exception in get_conversation_url: {e}")
1060
- raise
1061
 
1062
- # The send_message function is intentionally removed as it is not used in this WebSocket architecture.
 
 
1063
 
1064
- def calculate_cost(duration_seconds):
1065
- """Calculate cost based on conversation duration"""
1066
- minutes = (duration_seconds + 59) // 60
1067
- return minutes * CREDITS_PER_MIN
1068
 
1069
- # Your existing Flask route - no changes needed
1070
- @app.route('/api/projects/<project_id>/start-conversation', methods=['POST'])
1071
- def start_conversation(project_id):
1072
  """
1073
- Initiates a conversational AI session by providing the client with a secure WebSocket URL.
1074
- This backend route authorizes the user, creates/retrieves the agent, deducts an initiation
1075
- cost, and returns the URL. The client is responsible for the actual conversation.
1076
  """
1077
- start_time = time.time()
1078
- logger.info(f"[CONVERSATION] Starting conversation initiation process for project: {project_id}")
1079
 
1080
- try:
1081
- # Step 1: Authorization and Data Validation (Unchanged)
1082
- uid = verify_token(request.headers.get('Authorization'))
1083
- if not uid:
1084
- logger.error(f"[CONVERSATION] ERROR: Unauthorized access for project: {project_id}")
1085
- return jsonify({'error': 'Unauthorized'}), 401
1086
 
1087
- user_ref = db_ref.child(f'users/{uid}')
1088
- user = user_ref.get()
1089
- if not user:
1090
- logger.error(f"[CONVERSATION] ERROR: User not found for uid: {uid}")
1091
- return jsonify({'error': 'User not found'}), 404
1092
 
1093
- project = db_ref.child(f'projects/{project_id}').get()
1094
- if not project or project.get('uid') != uid:
1095
- logger.error(f"[CONVERSATION] ERROR: Project not found or access denied for user {uid}")
1096
- return jsonify({'error': 'Project not found'}), 404
1097
 
1098
- data = request.get_json()
1099
- if data is None:
1100
- logger.error(f"[CONVERSATION] ERROR: No JSON data received")
1101
- return jsonify({'error': 'No JSON data provided'}), 400
1102
-
1103
- # The initial message is noted but will be sent by the client, not the backend.
1104
- initial_message = data.get('message', 'Hello, I need help with my DIY project.')
1105
- logger.info(f"[CONVERSATION] User's initial message (to be sent by client): {initial_message[:100]}{'...' if len(initial_message) > 100 else ''}")
1106
-
1107
- # Step 2: Initialize Handler and Get Agent (Unchanged)
1108
- ai_handler = ConversationalAIHandler()
1109
- agent_id = ai_handler.create_or_get_agent(project_id, project)
1110
- logger.info(f"[CONVERSATION] Agent ready: {agent_id}")
1111
-
1112
- # Step 3: Get the Secure WebSocket URL (This is the new core logic)
1113
- # This replaces the old start_conversation and send_message calls.
1114
- websocket_url = ai_handler.get_conversation_url(agent_id)
1115
- logger.info(f"[CONVERSATION] WebSocket URL received from ElevenLabs.")
1116
-
1117
- # Step 4: Calculate Cost and Deduct Credits (Logic adapted for initiation fee)
1118
- # The cost is now for the service of setting up the session.
1119
- duration = time.time() - start_time
1120
- cost = calculate_cost(duration) # Or a flat fee, e.g., cost = CREDITS_PER_MIN
1121
-
1122
- user_credits = user.get('credits', 0)
1123
- if user_credits < cost:
1124
- logger.error(f"[CONVERSATION] ERROR: Insufficient credits for session initiation.")
1125
- return jsonify({'error': 'Insufficient credits', 'needed': cost}), 402
1126
-
1127
- new_credits = user_credits - cost
1128
- user_ref.update({'credits': new_credits})
1129
- logger.info(f"[CONVERSATION] Billed {cost} credits for session initiation. Remaining credits: {new_credits}")
1130
-
1131
- # Step 5: Log the Session Initiation
1132
- # We log that a session was started, but not the back-and-forth messages.
1133
- conversation_log_id = f"{project_id}_{int(start_time)}"
1134
- conversation_data = {
1135
- 'project_id': project_id,
1136
- 'uid': uid,
1137
- 'agent_id': agent_id,
1138
- 'conversation_log_id': conversation_log_id, # Self-reference
1139
- 'initial_message_prompt': initial_message,
1140
- 'duration_seconds_backend': duration, # Log backend processing time
1141
- 'credits_used': cost,
1142
- 'created_at': int(start_time)
1143
- }
1144
- db_ref.child(f'conversations/{conversation_log_id}').set(conversation_data)
1145
-
1146
- # Step 6: Return the WebSocket URL to the Client
1147
- # The client will use this URL to establish a direct connection for the conversation.
1148
- return jsonify({
1149
- 'websocket_url': websocket_url,
1150
- 'agent_id': agent_id,
1151
- 'conversation_log_id': conversation_log_id,
1152
- 'status': 'success',
1153
- 'durationSeconds': round(duration, 1),
1154
- 'creditsDeducted': cost,
1155
- 'remainingCredits': new_credits
1156
- }), 200
1157
 
1158
- except Exception as e:
1159
- total_duration = time.time() - start_time
1160
- logger.error(f"[CONVERSATION] CRITICAL ERROR in start_conversation route: {str(e)}")
1161
- logger.error(f"[CONVERSATION] Exception type: {type(e).__name__}")
1162
- return jsonify({'error': 'Failed to initiate conversation'}), 500
1163
-
1164
- @app.route('/api/webhook/agent/<project_id>', methods=['POST'])
1165
- def agent_webhook(project_id):
1166
- """Webhook to handle agent events"""
1167
  try:
1168
- data = request.get_json()
1169
- event_type = data.get('event_type')
1170
-
1171
- # Log the webhook event
1172
- logger.info(f"Agent webhook for project {project_id}: {event_type}")
 
 
 
 
1173
 
1174
- # Store webhook data for debugging/monitoring
1175
- db_ref.child(f'webhooks/{project_id}/{int(time.time())}').set({
1176
- 'event_type': event_type,
1177
- 'data': data,
1178
- 'timestamp': int(time.time())
1179
- })
1180
 
1181
- return jsonify({'status': 'received'}), 200
 
1182
 
1183
- except Exception as e:
1184
- logger.error(f"Webhook error: {str(e)}")
1185
- return jsonify({'error': 'Webhook processing failed'}), 500
1186
-
1187
- @app.route('/api/projects/<project_id>/continue-conversation', methods=['POST'])
1188
- def continue_conversation(project_id):
1189
- """Continue an existing conversation"""
1190
- try:
1191
- uid = verify_token(request.headers.get('Authorization'))
1192
- if not uid:
1193
- return jsonify({'error': 'Unauthorized'}), 401
1194
-
1195
- data = request.get_json()
1196
- agent_id = data.get('agent_id')
1197
- message = data.get('message')
1198
-
1199
- if not agent_id or not message:
1200
- return jsonify({'error': 'agent_id and message required'}), 400
1201
-
1202
- ai_handler = ConversationalAIHandler()
1203
- result = ai_handler.simulate_conversation(agent_id, message)
1204
 
 
1205
  return jsonify({
1206
- 'simulation_result': result,
1207
- 'timestamp': int(time.time())
 
1208
  }), 200
1209
 
1210
  except Exception as e:
1211
- logger.error(f"Error continuing conversation: {str(e)}")
1212
- return jsonify({'error': 'Failed to continue conversation'}), 500
1213
 
1214
 
1215
  # -----------------------------------------------------------------------------
 
951
  #------------------------
952
  # AI phone call ElevenLabs
953
  #-------------------------
954
+ import math
955
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
956
 
 
 
 
 
957
 
958
+ AGENT_ID = os.getenv("AGENT_ID", "agent_01jy1vv1hqe2b9x0yyp6ayrdxj")
959
+ ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")
 
 
 
 
960
 
961
+ @app.route('/api/projects/<project_id>/initiate-call', methods=['POST'])
962
+ def initiate_call(project_id):
963
+ """
964
+ Securely fetches a signed URL from ElevenLabs for the client-side SDK.
965
+ """
966
+ logger.info(f"[INITIATE] Received request for project: {project_id}")
967
+
968
+ # Your real authentication logic should be active
969
+ uid = verify_token(request.headers.get('Authorization'))
970
+ if not uid:
971
+ return jsonify({'error': 'Unauthorized'}), 401
972
 
973
+ if not ELEVENLABS_API_KEY:
974
+ logger.error("[INITIATE] ELEVENLABS_API_KEY is not set on the server.")
975
+ return jsonify({'error': 'Server configuration error.'}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
 
977
+ url = f"https://api.elevenlabs.io/v1/convai/conversation/get-signed-url?agent_id={AGENT_ID}"
978
+ headers = {"xi-api-key": ELEVENLABS_API_KEY}
 
979
 
980
+ try:
981
+ response = requests.get(url, headers=headers, timeout=15)
982
+ response.raise_for_status()
 
 
 
 
 
 
 
 
983
 
984
+ data = response.json()
985
+ signed_url = data.get("signed_url")
 
986
 
987
+ if not signed_url:
988
+ logger.error("[INITIATE] ElevenLabs response missing 'signed_url'.")
989
+ return jsonify({'error': 'Failed to retrieve session URL from provider.'}), 502
 
 
990
 
991
+ logger.info("[INITIATE] Successfully retrieved signed URL.")
992
+ return jsonify({"signed_url": signed_url}), 200
 
 
 
 
993
 
994
+ except requests.exceptions.RequestException as e:
995
+ logger.error(f"[INITIATE] Error calling ElevenLabs API: {e}")
996
+ return jsonify({'error': 'Could not connect to AI service provider.'}), 504
997
 
 
 
 
 
998
 
999
+ @app.route('/api/projects/<project_id>/log-call-usage', methods=['POST'])
1000
+ def log_call_usage(project_id):
 
1001
  """
1002
+ Calculates and deducts credits from a user's account in Firebase
1003
+ after a call is completed. This is the production-ready version.
 
1004
  """
1005
+ logger.info(f"[LOGGING] Received usage log for project: {project_id}")
 
1006
 
1007
+ # Step 1: Authenticate the user and get their unique ID
1008
+ uid = verify_token(request.headers.get('Authorization'))
1009
+ if not uid:
1010
+ return jsonify({'error': 'Unauthorized'}), 401
 
 
1011
 
1012
+ data = request.get_json()
1013
+ duration_seconds = data.get("durationSeconds")
 
 
 
1014
 
1015
+ if duration_seconds is None or not isinstance(duration_seconds, (int, float)):
1016
+ return jsonify({'error': 'Invalid duration provided.'}), 400
 
 
1017
 
1018
+ # Step 2: Calculate credit cost (3 credits per minute, always rounded up)
1019
+ minutes = math.ceil(duration_seconds / 60)
1020
+ cost = minutes * 3
1021
+
1022
+ logger.info(f"[LOGGING] User '{uid}' call duration: {duration_seconds:.2f}s, rounded to {minutes} minute(s). Cost: {cost} credits.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
 
 
 
 
 
 
 
 
 
 
1024
  try:
1025
+ # Step 3: Perform the database transaction
1026
+ user_ref = db_ref.child(f'users/{uid}')
1027
+ user_data = user_ref.get()
1028
+
1029
+ if user_data is None:
1030
+ logger.error(f"[LOGGING] User with UID '{uid}' not found in the database.")
1031
+ return jsonify({'error': 'User not found.'}), 404
1032
+
1033
+ current_credits = user_data.get('credits', 0)
1034
 
1035
+ # Calculate the new balance, ensuring it doesn't go below zero.
1036
+ new_credits = max(0, current_credits - cost)
 
 
 
 
1037
 
1038
+ # Update the user's credit balance in Firebase
1039
+ user_ref.update({'credits': new_credits})
1040
 
1041
+ logger.info(f"[LOGGING] Successfully updated credits for user '{uid}'. Old: {current_credits}, New: {new_credits}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1042
 
1043
+ # Step 4: Return the successful response with the actual new balance
1044
  return jsonify({
1045
+ "status": "success",
1046
+ "creditsDeducted": cost,
1047
+ "remainingCredits": new_credits
1048
  }), 200
1049
 
1050
  except Exception as e:
1051
+ logger.error(f"[LOGGING] A database error occurred for user '{uid}': {e}")
1052
+ return jsonify({'error': 'A server error occurred while updating credits.'}), 500
1053
 
1054
 
1055
  # -----------------------------------------------------------------------------