File size: 17,512 Bytes
a44e6f7
 
 
34f4c74
a44e6f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34f4c74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a44e6f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f46acd8
a44e6f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8cb6e61
a44e6f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
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