SalesDocSpace / app.py
devcool20's picture
Update app.py
34f4c74 verified
import os
import sys
from flask import Flask, request, jsonify
from flask_cors import CORS # Keep this import, but we'll use it differently for now
import numpy as np
import json
import google.api_core.exceptions
from dotenv import load_dotenv
import google.generativeai as genai
from google.generativeai.types import GenerationConfig
print("--- Script Start: app.py ---")
# Load environment variables for local testing (Hugging Face handles secrets directly)
load_dotenv()
app = Flask(__name__)
# --- AGGRESSIVE CORS CONFIGURATION ---
# This attempts to ensure CORS headers are *always* sent.
# Define your allowed origin explicitly.
ALLOWED_ORIGIN = "https://sales-doc.vercel.app"
@app.before_request
def handle_options_requests():
"""Handle CORS preflight OPTIONS requests."""
if request.method == 'OPTIONS':
response = app.make_response('')
response.headers.add('Access-Control-Allow-Origin', ALLOWED_ORIGIN)
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
response.headers.add('Access-Control-Allow-Credentials', 'true') # If your frontend sends credentials
response.headers.add('Access-Control-Max-Age', '86400') # Cache preflight for 24 hours
print(f"DEBUG: Handling OPTIONS preflight request from {request.origin}")
print(f"DEBUG: Setting CORS headers for OPTIONS: {response.headers}")
return response
@app.after_request
def add_cors_headers(response):
"""Add CORS headers to all responses."""
response.headers.add('Access-Control-Allow-Origin', ALLOWED_ORIGIN)
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
response.headers.add('Access-Control-Allow-Credentials', 'true')
print(f"DEBUG: Adding CORS headers to response for origin {request.origin if request.origin else 'N/A'}")
print(f"DEBUG: Response headers: {response.headers}")
return response
# You can comment out the Flask-CORS extension initialization if using manual headers,
# or keep it if it's not causing conflicts. For this aggressive approach, we'll
# rely on the manual headers.
# CORS(app, resources={r"/*": {"origins": ALLOWED_ORIGIN, "allow_headers": ["Content-Type", "Authorization"]}})
# --- Global Model Instances ---
sales_agent = None
gemini_model = None
gemini_api_key_status = "Not Set" # Track API key status for logs
# --- Configure API Keys & Initialize Models ---
print("\n--- Starting API Key and Model Initialization ---")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if GEMINI_API_KEY:
try:
genai.configure(api_key=GEMINI_API_KEY)
gemini_api_key_status = "Configured"
print("Gemini API Key detected and configured.")
except Exception as e:
gemini_api_key_status = f"Configuration Failed: {e}"
print(f"ERROR: Failed to configure Gemini API: {e}")
else:
print("WARNING: GEMINI_API_KEY environment variable not found. Gemini LLM features will be disabled.")
gemini_api_key_status = "Missing"
# --- DEEPMOST IMPORT FIX ---
try:
from deepmost import sales
print("Debug Point: Successfully imported deepmost.sales module.")
except ImportError as e:
print(f"CRITICAL ERROR: Failed to import deepmost.sales module: {e}")
print("This means the 'deepmost' library is not correctly installed or its path is wrong.")
print("SalesRLAgent core model functionality will be disabled.")
sales = None # Set sales to None if import fails, to prevent NameError later
# DeepMost SalesRLAgent Core Model Initialization
print("Debug Point: Attempting to instantiate sales.Agent (core RL model).")
if sales is not None:
try:
# Relying on Dockerfile to make /.deepmost writable
sales_agent = sales.Agent(
model_path="https://huggingface.co/DeepMostInnovations/sales-conversion-model-reinf-learning/resolve/main/sales_conversion_model.zip",
auto_download=True,
use_gpu=False
)
if sales_agent is not None:
print("Debug Point: DeepMost SalesRLAgent core model initialized successfully.")
else:
print("ERROR: DeepMost SalesRLAgent core model failed to initialize after constructor call (returned None).")
except Exception as e:
print(f"CRITICAL ERROR: DeepMost SalesRLAgent core model loading or instantiation failed.")
print(f"Error Type: {type(e).__name__}")
print(f"Error Message: {e}")
import traceback
traceback.print_exc()
sales_agent = None
print("DeepMost model initialization set to None due to error.")
else:
print("DeepMost SalesRLAgent core model instantiation skipped because 'sales' module could not be imported.")
# Gemini LLM (1.5 Flash) Initialization
print("\nDebug Point: Attempting to initialize Gemini 1.5 Flash model.")
if GEMINI_API_KEY:
try:
gemini_model = genai.GenerativeModel('gemini-1.5-flash-latest')
test_response = gemini_model.generate_content("Hello.", generation_config=GenerationConfig(max_output_tokens=10))
print(f"Debug Point: Gemini 1.5 Flash test response: {test_response.text[:50]}...")
print("Debug Point: Gemini LLM (1.5 Flash) initialized successfully.")
except Exception as e:
print(f"CRITICAL ERROR: Gemini LLM (1.5 Flash) initialization failed.")
print(f"Error Type: {type(e).__name__}")
print(f"Error Message: {e}")
print("Ensure your GEMINI_API_KEY is correct and has access to Gemini 1.5 Flash.")
import traceback
traceback.print_exc()
gemini_model = None
print(f"Gemini Model Status: {'Initialized' if gemini_model else 'Failed to Initialize'}")
else:
print("Debug Point: Skipping Gemini LLM initialization because GEMINI_API_KEY is not set.")
print("Gemini Model Status: Disabled (API Key Missing)")
print("--- Finished Model Initialization Block ---\n")
# --- Flask Routes (API Endpoints only) ---
@app.route('/analyze_conversation', methods=['POST'])
def analyze_conversation():
if sales_agent is None:
print("ERROR: API call received for analyze_conversation but sales_agent (core) is None.")
return jsonify({"error": "SalesRLAgent core model not initialized on backend. Check Space logs for DeepMost initialization errors."}), 500
try:
data = request.get_json()
if not data or 'conversation' not in data:
return jsonify({"error": "Invalid request. 'conversation' field is required."}), 400
conversation = data['conversation']
if not isinstance(conversation, list) or not all(isinstance(turn, str) for turn in conversation):
return jsonify({"error": "'conversation' must be a list of strings."}), 400
print(f"Processing /analyze_conversation for: {conversation}")
all_analysis_results = []
full_conversation_so_far = []
for i, turn_message in enumerate(conversation):
full_conversation_so_far.append(turn_message)
deepmost_analysis = sales_agent.analyze_conversation_progression(full_conversation_so_far, print_results=False)
probability = 0.0
if deepmost_analysis and len(deepmost_analysis) > 0:
probability = deepmost_analysis[-1]['probability']
llm_metrics = {}
llm_per_turn_suggestion = ""
turn_result = {
"turn": i + 1,
"speaker": turn_message.split(":")[0].strip() if ":" in turn_message else "Unknown",
"message": turn_message,
"probability": probability,
"status": "calculated",
"metrics": llm_metrics,
"llm_per_turn_suggestion": llm_per_turn_suggestion
}
all_analysis_results.append(turn_result)
print(f"Successfully processed /analyze_conversation. Returning {len(all_analysis_results)} results.")
return jsonify({"results": all_analysis_results, "llm_advice_pending": True}), 200
except Exception as e:
print(f"ERROR: Exception during /analyze_conversation: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": f"An error occurred during analysis: {str(e)}"}), 500
@app.route('/get_llm_advice', methods=['POST'])
def get_llm_advice():
if gemini_model is None:
print("ERROR: LLM advice requested but Gemini LLM is not initialized or available.")
return jsonify({"points": ["LLM advice unavailable. Gemini failed to load on backend. Check Space logs or add GEMINI_API_KEY secret."]}), 500
try:
data = request.get_json()
conversation = data.get('conversation', [])
if not conversation:
return jsonify({"points": ["No conversation provided for LLM advice."]}), 400
full_convo_text = "\n".join(conversation)
advice_prompt = (
f"Analyze the entire following sales conversation:\n\n"
f"{full_convo_text}\n\n"
f"As a concise sales coach, provide actionable advice to the salesperson on how to best progress this sales call towards a successful outcome. "
f"Provide this advice as a JSON object with a single key 'points' which is an array of strings, where each string is a distinct, actionable bullet point. "
f"Do NOT include any other text outside the JSON object. Ensure the JSON is well-formed and complete."
)
print(f"Processing /get_llm_advice. Prompting Gemini: {advice_prompt[:200]}...")
try:
gemini_response = gemini_model.generate_content(
[advice_prompt],
generation_config=GenerationConfig(
response_mime_type="application/json",
response_schema={"type": "OBJECT", "properties": {"points": {"type": "ARRAY", "items": {"type": "STRING"}}}, "required": ["points"]},
max_output_tokens=300,
temperature=0.6
)
)
raw_json_string = ""
if gemini_response and gemini_response.candidates and len(gemini_response.candidates) > 0 and \
gemini_response.candidates[0].content and gemini_response.candidates[0].content.parts and \
len(gemini_response.candidates[0].content.parts) > 0:
raw_json_string = gemini_response.candidates[0].content.parts[0].text.strip()
print(f"Raw LLM JSON response: {raw_json_string}")
else:
print("WARNING: Empty or malformed LLM response for overall advice.")
return jsonify({"points": ["LLM returned an empty or malformed response. Try again or check conversation length."]}), 200
parsed_advice = {}
try:
parsed_advice = json.loads(raw_json_string)
if "points" in parsed_advice and isinstance(parsed_advice["points"], list):
print(f"Successfully parsed Gemini advice: {parsed_advice}")
return jsonify(parsed_advice), 200
else:
print(f"WARNING: LLM did not return 'points' array in structured advice: {raw_json_string}")
return jsonify({"points": ["LLM response was not structured as expected (missing 'points' array). Raw: " + raw_json_string[:100] + "..."]}), 200
except json.JSONDecodeError as json_e:
print(f"ERROR: JSON parsing error for overall advice: {json_e}. Raw string: {raw_json_string}")
return jsonify({"points": ["Error parsing LLM JSON advice. This happens with incomplete LLM responses (e.g., due to API rate limits or max tokens). Please try a shorter conversation or wait a moment. Raw response starts with: " + raw_json_string[:100] + "..."]})
except Exception as parse_e:
print(f"ERROR: General error during JSON parsing attempt for chat_llm (Gemini): {parse_e}. Raw string: {raw_json_string}")
return jsonify({"points": ["General error with LLM JSON parsing. Raw response starts with: " + raw_json_string[:100] + "..."]})
except google.api_core.exceptions.ResourceExhausted as quota_e:
print(f"ERROR: Quota Exceeded for LLM advice: {quota_e}")
return jsonify({"points": ["Quota Exceeded: Cannot generate overall LLM advice due to API rate limits. Please try again in a minute or two."]}), 200
except Exception as e:
print(f"ERROR: Exception generating structured Gemini advice: {e}")
import traceback
traceback.print_exc()
return jsonify({"points": [f"Error generating LLM advice: {type(e).__name__} - {e}"]}), 200
except Exception as e:
print(f"ERROR: An unexpected error occurred in /get_llm_advice: {e}")
import traceback
traceback.print_exc()
return jsonify({"points": [f"An unexpected error occurred: {type(e).__name__} - {e}"]}), 500
@app.route('/chat_llm', methods=['POST'])
def chat_llm():
if gemini_model is None:
print("ERROR: Gemini LLM instance is not initialized or available for chat.")
return jsonify({"error": "LLM chat functionality unavailable. Gemini failed to load."}), 500
try:
data = request.get_json()
user_message = data.get('message', '')
if not user_message:
return jsonify({"error": "No message provided."}), 400
print(f"Processing /chat_llm. Received message: {user_message}")
general_chat_prompt = f"Respond to the following message concisely: '{user_message}'"
chat_response_obj = gemini_model.generate_content(
general_chat_prompt,
generation_config=GenerationConfig(max_output_tokens=150, temperature=0.7)
)
chat_response = chat_response_obj.text.strip()
print(f"Gemini Raw Chat Response: {chat_response}")
json_prompt = (
f"Analyze the following message: '{user_message}'. "
f"Provide a JSON object with 'summary', 'sentiment' (positive/neutral/negative), "
f"and 'keywords' (array of strings). Do not include any other text outside the JSON block."
)
json_response_obj = gemini_model.generate_content(
[json_prompt],
generation_config=GenerationConfig(
response_mime_type="application/json",
max_output_tokens=200,
temperature=0.1
)
)
json_response = json_response_obj.text.strip()
print(f"Gemini Raw JSON Prompt Response: {json_response}")
parsed_json_output = None
try:
parsed_json_output = json.loads(json_response)
print(f"Parsed JSON from Gemini chat: {parsed_json_output}")
except json.JSONDecodeError as e:
print(f"ERROR: JSON parsing error for chat_llm (Gemini): {e}. Raw string: {json_response}")
except Exception as e:
print(f"ERROR: General error during JSON parsing attempt for chat_llm (Gemini): {e}. Raw string: {json_response}")
return jsonify({
"user_message": user_message,
"raw_chat_response": chat_response,
"raw_json_prompt_response": json_response,
"parsed_json_metrics": parsed_json_output,
"status": "success"
}), 200
except Exception as e:
print(f"ERROR: Error during LLM chat: {e}")
import traceback
traceback.print_exc()
return jsonify({"error": f"An error occurred during LLM chat: {str(e)}"}), 500
# Health check endpoint for Hugging Face Spaces (optional, but good practice)
@app.route('/health', methods=['GET'])
def health_check():
status = {
"status": "up",
"deepmost_model_initialized": sales_agent is not None,
"gemini_llm_initialized": gemini_model is not None,
"gemini_api_key_status": gemini_api_key_status,
"message": "Application is running"
}
# Provide more detail if a component failed
if sales_agent is None:
status["message"] = "Application running, but DeepMost model failed to initialize."
status["status"] = "degraded"
if gemini_model is None and gemini_api_key_status != "Missing": # Only degraded if API key was provided but init failed
status["message"] = "Application running, but Gemini LLM failed to initialize."
status["status"] = "degraded"
elif gemini_model is None and gemini_api_key_status == "Missing":
status["message"] = "Application running. Gemini LLM disabled (no API key)."
print(f"Health check requested. Status: {status}")
return jsonify(status), 200
# --- Main Execution Block ---
if __name__ == '__main__':
try:
print("Attempting to start Flask app (this block is primarily for local execution).")
print("Application setup complete. Expecting Gunicorn to take over.")
except Exception as startup_exception:
print(f"CRITICAL: An unhandled exception occurred during Flask app setup: {startup_exception}")
import traceback
traceback.print_exc()
sys.exit(1) # Exit with error code if startup fails