rairo commited on
Commit
7443fd5
·
verified ·
1 Parent(s): 05f9336

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +67 -109
main.py CHANGED
@@ -969,10 +969,11 @@ KNOWN_VOICE_IDS = {
969
 
970
  class ConversationalAIHandler:
971
  def __init__(self):
972
- self.base_url = "https://api.elevenlabs.io/v1"
 
973
  self.api_key = os.getenv("ELEVENLABS_API_KEY")
974
  if not self.api_key:
975
- logger.critical("[AGENT] ELEVENLABS_API_KEY environment variable not set.")
976
  raise ValueError("ELEVENLABS_API_KEY environment variable not set.")
977
  self.headers = {
978
  "xi-api-key": self.api_key,
@@ -980,41 +981,29 @@ class ConversationalAIHandler:
980
  }
981
 
982
  def get_british_male_voice_id(self):
983
- logger.info(f"[VOICE] Using known voice ID for Daniel.")
 
984
  return KNOWN_VOICE_IDS.get("Daniel", "onwK4e9ZLuTAKqWW03F9")
985
 
986
  def create_or_get_agent(self, project_id, project_data):
 
987
  try:
988
- logger.info(f"[AGENT] Starting agent creation/retrieval for project: {project_id}")
989
 
990
  existing_agent_id = db_ref.child(f'projects/{project_id}/agent_id').get()
991
  if existing_agent_id:
992
- logger.info(f"[AGENT] Found existing agent ID in DB: {existing_agent_id}. Verifying...")
993
- verify_url = f"{self.base_url}/convai/agents/{existing_agent_id}"
994
- try:
995
- response = requests.get(verify_url, headers=self.headers, timeout=15)
996
- if response.status_code == 200:
997
- logger.info(f"[AGENT] Existing agent {existing_agent_id} verified and active.")
998
- return existing_agent_id
999
- else:
1000
- logger.warning(f"[AGENT] Verification failed for agent {existing_agent_id} (Status: {response.status_code}). Creating a new one.")
1001
- except Exception as e:
1002
- logger.warning(f"[AGENT] Error verifying agent: {e}. Creating a new one.")
1003
-
1004
- logger.info(f"[AGENT] Creating new agent as per Convai API documentation.")
1005
  voice_id = self.get_british_male_voice_id()
1006
 
1007
  diy_expert_prompt = f"""
1008
- You are an experienced DIY expert with years of hands-on experience in Home Appliance Repair, Automotive Maintenance, Gardening & Urban Farming,
1009
- Upcycling & Sustainable Crafts, or DIY Project Creation. You speak in a friendly, knowledgeable British manner and provide
1010
- practical, actionable advice. You're working on this specific project:
1011
- Project: {project_data.get('projectTitle', 'DIY Project')}
1012
- Description: {project_data.get('projectDescription', '')}
1013
- Initial Plan: {project_data.get('initialPlan', '')}
1014
- Provide helpful, step-by-step guidance while being encouraging and safety-conscious.
1015
- Ask clarifying questions when needed and share relevant tips from your experience.
1016
  """
1017
-
1018
  agent_payload = {
1019
  "conversation_config": {
1020
  "name": f"DIY Expert - {project_data.get('projectTitle', project_id)}",
@@ -1024,83 +1013,53 @@ class ConversationalAIHandler:
1024
  "prompt": diy_expert_prompt
1025
  }
1026
  }
1027
-
1028
- creation_url = f"{self.base_url}/convai/agents/create"
1029
- logger.info(f"[AGENT] Posting to official endpoint: {creation_url}")
1030
-
1031
  response = requests.post(creation_url, headers=self.headers, json=agent_payload, timeout=30)
1032
 
1033
  if response.status_code == 200:
1034
  agent_info = response.json()
1035
  agent_id = agent_info.get("agent_id")
1036
- logger.info(f"[AGENT] New agent created successfully: {agent_id}")
1037
  db_ref.child(f'projects/{project_id}').update({'agent_id': agent_id})
1038
  return agent_id
1039
  else:
1040
- logger.error(f"[AGENT] Failed to create agent. Status: {response.status_code}, Response: {response.text}")
1041
- logger.error("[AGENT] ACTION REQUIRED: This error may indicate a subscription issue or malformed payload.")
1042
  raise Exception(f"Failed to create agent: HTTP {response.status_code} - {response.text}")
1043
 
1044
  except Exception as e:
1045
- logger.error(f"[AGENT] An exception occurred in create_or_get_agent: {e}")
1046
  raise
1047
 
1048
- def start_conversation(self, agent_id):
1049
  """
1050
- For private agents, fetch a signed WebSocket URL via REST, then
1051
- initiate the connection client-side. Returns the signed URL and
1052
- a conversation_id for tracking or logging.
1053
  """
1054
  try:
1055
- logger.info(f"[CONVERSATION] Requesting signed URL for agent: {agent_id}")
1056
- url = f"{self.base_url}/convai/conversation/get-signed-url"
1057
  params = {"agent_id": agent_id}
 
1058
  resp = requests.get(url, headers=self.headers, params=params, timeout=15)
1059
 
1060
  if resp.status_code != 200:
1061
- logger.error(f"[CONVERSATION] Signed-URL request failed ({resp.status_code}): {resp.text}")
1062
  raise Exception(f"Could not get signed URL: {resp.text}")
1063
 
1064
  data = resp.json()
1065
  signed_url = data.get("signed_url")
1066
  if not signed_url:
1067
- logger.error("[CONVERSATION] Missing 'signed_url' in response.")
1068
- raise Exception("Invalid signed URL response")
1069
-
1070
- # You can optionally list the conversations to extract the conversation_id:
1071
- logger.info("[CONVERSATION] Fetching existing conversations list for agent...")
1072
- list_resp = requests.get(f"{self.base_url}/convai/conversations",
1073
- headers=self.headers,
1074
- params={"agent_id": agent_id}, timeout=15)
1075
- if list_resp.status_code == 200:
1076
- convs = list_resp.json().get("conversations", [])
1077
- conv_ids = [c["conversation_id"] for c in convs]
1078
- logger.info(f"[CONVERSATION] Agent has {len(conv_ids)} existing conversations.")
1079
- else:
1080
- logger.warning(f"[CONVERSATION] Couldn't list conversations: HTTP {list_resp.status_code}")
1081
 
1082
- return signed_url, conv_ids if 'conv_ids' in locals() else []
 
 
1083
  except Exception as e:
1084
- logger.error(f"[CONVERSATION] Exception in start_conversation: {e}")
1085
  raise
1086
 
1087
- def send_message(self, agent_id, conversation_id, message):
1088
- try:
1089
- logger.info(f"[MESSAGE] Sending message to conversation: {conversation_id}")
1090
- message_url = f"{self.base_url}/conversations/{conversation_id}/messages"
1091
- payload = {"text": message}
1092
-
1093
- response = requests.post(message_url, headers=self.headers, json=payload, timeout=30)
1094
-
1095
- if response.status_code == 200:
1096
- logger.info(f"[MESSAGE] Message sent successfully.")
1097
- return response.json()
1098
- else:
1099
- logger.error(f"[MESSAGE] Failed to send message. Status: {response.status_code}, Response: {response.text}")
1100
- raise Exception(f"Failed to send message: {response.text}")
1101
- except Exception as e:
1102
- logger.error(f"[MESSAGE] An exception occurred in send_message: {e}")
1103
- raise
1104
 
1105
  def calculate_cost(duration_seconds):
1106
  """Calculate cost based on conversation duration"""
@@ -1110,88 +1069,87 @@ def calculate_cost(duration_seconds):
1110
  # Your existing Flask route - no changes needed
1111
  @app.route('/api/projects/<project_id>/start-conversation', methods=['POST'])
1112
  def start_conversation(project_id):
1113
- """Start a conversational AI session for a project"""
 
 
 
 
1114
  start_time = time.time()
1115
- logger.info(f"[CONVERSATION] Starting conversation process for project: {project_id}")
1116
 
1117
  try:
1118
- # Authorization
1119
  uid = verify_token(request.headers.get('Authorization'))
1120
  if not uid:
1121
- logger.error(f"[CONVERSATION] ERROR: Unauthorized access attempt for project: {project_id}")
1122
  return jsonify({'error': 'Unauthorized'}), 401
1123
 
1124
- # Get user data
1125
  user_ref = db_ref.child(f'users/{uid}')
1126
  user = user_ref.get()
1127
  if not user:
1128
  logger.error(f"[CONVERSATION] ERROR: User not found for uid: {uid}")
1129
  return jsonify({'error': 'User not found'}), 404
1130
 
1131
- # Get project data
1132
  project = db_ref.child(f'projects/{project_id}').get()
1133
  if not project or project.get('uid') != uid:
1134
- logger.error(f"[CONVERSATION] ERROR: Project not found or access denied")
1135
  return jsonify({'error': 'Project not found'}), 404
1136
 
1137
- # Parse request data
1138
  data = request.get_json()
1139
  if data is None:
1140
  logger.error(f"[CONVERSATION] ERROR: No JSON data received")
1141
  return jsonify({'error': 'No JSON data provided'}), 400
1142
 
 
1143
  initial_message = data.get('message', 'Hello, I need help with my DIY project.')
1144
- logger.info(f"[CONVERSATION] Initial message: {initial_message[:100]}{'...' if len(initial_message) > 100 else ''}")
1145
 
1146
- # Initialize AI Handler
1147
  ai_handler = ConversationalAIHandler()
1148
-
1149
- # Create or get agent
1150
  agent_id = ai_handler.create_or_get_agent(project_id, project)
1151
  logger.info(f"[CONVERSATION] Agent ready: {agent_id}")
1152
 
1153
- # Start conversation
1154
- conversation_id = ai_handler.start_conversation(agent_id)
1155
- logger.info(f"[CONVERSATION] Conversation started: {conversation_id}")
 
1156
 
1157
- # Send initial message
1158
- response_data = ai_handler.send_message(agent_id, conversation_id, initial_message)
1159
- logger.info(f"[CONVERSATION] Message sent successfully")
1160
-
1161
- # Calculate cost and duration
1162
  duration = time.time() - start_time
1163
- cost = calculate_cost(duration)
1164
 
1165
- # Check and deduct credits
1166
  user_credits = user.get('credits', 0)
1167
  if user_credits < cost:
1168
- logger.error(f"[CONVERSATION] ERROR: Insufficient credits")
1169
  return jsonify({'error': 'Insufficient credits', 'needed': cost}), 402
1170
 
1171
  new_credits = user_credits - cost
1172
  user_ref.update({'credits': new_credits})
1173
-
1174
- # Log conversation
 
 
1175
  conversation_log_id = f"{project_id}_{int(start_time)}"
1176
  conversation_data = {
1177
  'project_id': project_id,
1178
  'uid': uid,
1179
  'agent_id': agent_id,
1180
- 'conversation_id': conversation_id,
1181
- 'initial_message': initial_message,
1182
- 'response': response_data,
1183
- 'duration_seconds': duration,
1184
  'credits_used': cost,
1185
  'created_at': int(start_time)
1186
  }
1187
  db_ref.child(f'conversations/{conversation_log_id}').set(conversation_data)
1188
 
1189
- # Return success response
 
1190
  return jsonify({
1191
- 'conversation_log_id': conversation_log_id,
1192
  'agent_id': agent_id,
1193
- 'conversation_id': conversation_id,
1194
- 'response': response_data,
1195
  'durationSeconds': round(duration, 1),
1196
  'creditsDeducted': cost,
1197
  'remainingCredits': new_credits
@@ -1199,9 +1157,9 @@ def start_conversation(project_id):
1199
 
1200
  except Exception as e:
1201
  total_duration = time.time() - start_time
1202
- logger.error(f"[CONVERSATION] CRITICAL ERROR: {str(e)}")
1203
  logger.error(f"[CONVERSATION] Exception type: {type(e).__name__}")
1204
- return jsonify({'error': 'Failed to start conversation'}), 500
1205
 
1206
  @app.route('/api/webhook/agent/<project_id>', methods=['POST'])
1207
  def agent_webhook(project_id):
 
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,
 
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)}",
 
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"""
 
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
 
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):