Update app.py
Browse files
app.py
CHANGED
@@ -1,220 +1,185 @@
|
|
1 |
-
|
2 |
-
|
3 |
import os
|
4 |
-
from dotenv import load_dotenv
|
5 |
import logging
|
6 |
-
import
|
7 |
-
from
|
8 |
-
|
9 |
-
|
10 |
-
load_dotenv()
|
11 |
|
12 |
# Configure logging
|
13 |
-
logging.basicConfig(
|
14 |
-
level=logging.DEBUG, # Set to DEBUG for detailed logs
|
15 |
-
format='%(asctime)s - %(levelname)s - %(message)s',
|
16 |
-
handlers=[
|
17 |
-
logging.FileHandler('app.log'),
|
18 |
-
logging.StreamHandler()
|
19 |
-
]
|
20 |
-
)
|
21 |
logger = logging.getLogger(__name__)
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
app = Flask(__name__)
|
24 |
-
app.secret_key = os.getenv('FLASK_SECRET_KEY', 'tPhZ8oXGBoadFBQXgUkSR2kDH')
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
"
|
30 |
-
"
|
31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
-
def get_salesforce_connection():
|
34 |
-
"""Establish a Salesforce connection with detailed error handling."""
|
35 |
try:
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
45 |
|
46 |
-
|
47 |
-
|
48 |
-
logger.
|
49 |
-
return
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
logger.error(f"Unexpected error connecting to Salesforce: {str(e)}")
|
58 |
-
raise Exception(f"Unable to connect to Salesforce: {str(e)}. Please check your configuration.")
|
59 |
-
|
60 |
-
|
61 |
-
def hash_password(password):
|
62 |
-
"""Hash a password using bcrypt."""
|
63 |
-
try:
|
64 |
-
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
65 |
except Exception as e:
|
66 |
-
logger.error(
|
67 |
-
|
68 |
-
|
69 |
-
def
|
70 |
-
"""
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
logger.error(
|
75 |
return False
|
76 |
|
77 |
-
@app.route('/')
|
78 |
-
def index():
|
79 |
-
if 'supervisor_id' not in session:
|
80 |
-
logger.info("User not logged in, redirecting to login page")
|
81 |
-
return redirect(url_for('login_page'))
|
82 |
-
return render_template('index.html')
|
83 |
-
|
84 |
-
@app.route('/login', methods=['GET'])
|
85 |
-
def login_page():
|
86 |
-
return render_template('login.html')
|
87 |
-
|
88 |
-
@app.route('/signup', methods=['GET'])
|
89 |
-
def signup_page():
|
90 |
-
return render_template('signup.html')
|
91 |
-
|
92 |
-
@app.route('/login', methods=['POST'])
|
93 |
-
def login():
|
94 |
-
data = request.get_json()
|
95 |
-
supervisor_id = data.get('supervisor_id')
|
96 |
-
password = data.get('password')
|
97 |
-
|
98 |
-
if not supervisor_id or not password:
|
99 |
-
logger.warning("Login failed: Supervisor ID and password are required")
|
100 |
-
return jsonify({"status": "error", "message": "Supervisor ID and password are required"}), 400
|
101 |
-
|
102 |
-
if supervisor_id == 'GUEST':
|
103 |
-
session['supervisor_id'] = 'GUEST'
|
104 |
-
logger.info("Guest login successful")
|
105 |
-
return jsonify({"status": "success", "message": "Logged in as guest"})
|
106 |
-
|
107 |
try:
|
108 |
-
sf =
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
return jsonify({"status": "error", "message": "No password set for this Supervisor ID"}), 401
|
124 |
-
|
125 |
-
if not verify_password(password, stored_password):
|
126 |
-
logger.warning(f"Invalid password for Supervisor ID: {supervisor_id}")
|
127 |
-
return jsonify({"status": "error", "message": "Invalid password"}), 401
|
128 |
-
|
129 |
-
session['supervisor_id'] = supervisor_id
|
130 |
-
logger.info(f"Login successful for {supervisor_id}")
|
131 |
-
return jsonify({"status": "success", "message": "Login successful"})
|
132 |
-
except Exception as e:
|
133 |
-
logger.error(f"Login error: {str(e)}")
|
134 |
-
return jsonify({"status": "error", "message": str(e)}), 500
|
135 |
-
|
136 |
-
@app.route('/signup', methods=['POST'])
|
137 |
-
def signup():
|
138 |
-
data = request.get_json()
|
139 |
-
supervisor_id = data.get('supervisor_id')
|
140 |
-
password = data.get('password')
|
141 |
-
|
142 |
-
if not supervisor_id or not password:
|
143 |
-
logger.warning("Signup failed: Supervisor ID and password are required")
|
144 |
-
return jsonify({"status": "error", "message": "Supervisor ID and password are required"}), 400
|
145 |
-
|
146 |
-
try:
|
147 |
-
sf = get_salesforce_connection()
|
148 |
-
logger.debug(f"Checking if Supervisor_ID__c {supervisor_id} already exists")
|
149 |
-
supervisor_id_escaped = quote(supervisor_id, safe='')
|
150 |
-
query = f"SELECT Id FROM Supervisor__c WHERE Name = '{supervisor_id_escaped}' LIMIT 1"
|
151 |
-
result = sf.query(query)
|
152 |
-
if result['records']:
|
153 |
-
logger.warning(f"Signup failed: Supervisor ID {supervisor_id} already exists")
|
154 |
-
return jsonify({"status": "error", "message": "Supervisor ID already exists"}), 400
|
155 |
-
|
156 |
-
hashed_password = hash_password(password)
|
157 |
-
logger.debug(f"Creating new Supervisor__c record for {supervisor_id}")
|
158 |
-
new_record = {
|
159 |
-
'Name': supervisor_id,
|
160 |
-
'Password__c': hashed_password
|
161 |
-
|
162 |
}
|
163 |
-
response = sf.Supervisor__c.create(new_record)
|
164 |
-
logger.debug(f"Salesforce create response: {response}")
|
165 |
|
166 |
-
|
167 |
-
|
168 |
-
|
|
|
|
|
|
|
169 |
|
170 |
-
session['supervisor_id'] = supervisor_id
|
171 |
-
logger.info(f"Signup successful for {supervisor_id}")
|
172 |
-
return jsonify({"status": "success", "message": "Signup successful, you are now logged in"})
|
173 |
except Exception as e:
|
174 |
-
logger.error(
|
175 |
-
return
|
176 |
-
|
177 |
-
@app.route('/logout', methods=['POST'])
|
178 |
-
def logout():
|
179 |
-
supervisor_id = session.get('supervisor_id', 'Unknown')
|
180 |
-
session.pop('supervisor_id', None)
|
181 |
-
logger.info(f"User {supervisor_id} logged out")
|
182 |
-
return jsonify({"status": "success", "message": "Logged out successfully"})
|
183 |
|
184 |
-
@app.route('/
|
185 |
-
def
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
190 |
|
|
|
|
|
|
|
|
|
|
|
191 |
try:
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
record = result['records'][0]
|
204 |
-
data = {
|
205 |
-
"supervisor_id": record['Supervisor_ID__c'],
|
206 |
-
"project_id": record['Project_ID__c'],
|
207 |
-
"last_login": str(datetime.now())
|
208 |
-
}
|
209 |
-
logger.info(f"Fetched data for supervisor {supervisor_id}")
|
210 |
-
return jsonify({"status": "success", "data": data})
|
211 |
else:
|
212 |
-
|
213 |
-
return jsonify({"status": "error", "message": "No data found for this supervisor"}), 404
|
214 |
except Exception as e:
|
215 |
-
logger.error(
|
216 |
return jsonify({"status": "error", "message": str(e)}), 500
|
217 |
|
218 |
-
|
219 |
-
|
220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import json
|
3 |
import os
|
|
|
4 |
import logging
|
5 |
+
from datetime import datetime
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
from simple_salesforce import Salesforce
|
8 |
+
from flask import Flask, jsonify, request, render_template, redirect, url_for
|
|
|
9 |
|
10 |
# Configure logging
|
11 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
logger = logging.getLogger(__name__)
|
13 |
|
14 |
+
# Load environment variables
|
15 |
+
load_dotenv()
|
16 |
+
|
17 |
+
# Hugging Face API configuration
|
18 |
+
HUGGING_FACE_API_URL = os.getenv("HUGGING_FACE_API_URL", "https://api-inference.huggingface.co/models/distilgpt2")
|
19 |
+
HUGGING_FACE_API_TOKEN = os.getenv("HUGGING_FACE_API_TOKEN")
|
20 |
+
|
21 |
+
# Salesforce configuration
|
22 |
+
SALESFORCE_USERNAME = os.getenv("SALESFORCE_USERNAME")
|
23 |
+
SALESFORCE_PASSWORD = os.getenv("SALESFORCE_PASSWORD")
|
24 |
+
SALESFORCE_SECURITY_TOKEN = os.getenv("SALESFORCE_SECURITY_TOKEN")
|
25 |
+
SALESFORCE_DOMAIN = os.getenv("SALESFORCE_DOMAIN", "login")
|
26 |
+
|
27 |
+
# Validate environment variables
|
28 |
+
if not HUGGING_FACE_API_TOKEN:
|
29 |
+
logger.error("HUGGING_FACE_API_TOKEN is not set")
|
30 |
+
raise ValueError("HUGGING_FACE_API_TOKEN environment variable is not set")
|
31 |
+
if not HUGGING_FACE_API_URL.startswith("https://api-inference.huggingface.co/models/"):
|
32 |
+
logger.error("Invalid HUGGING_FACE_API_URL: %s", HUGGING_FACE_API_URL)
|
33 |
+
raise ValueError("HUGGING_FACE_API_URL must point to a valid Hugging Face model")
|
34 |
+
if not all([SALESFORCE_USERNAME, SALESFORCE_PASSWORD, SALESFORCE_SECURITY_TOKEN]):
|
35 |
+
logger.error("Salesforce credentials are incomplete")
|
36 |
+
raise ValueError("Salesforce credentials must be set")
|
37 |
+
|
38 |
+
# Initialize Flask app
|
39 |
app = Flask(__name__)
|
|
|
40 |
|
41 |
+
def generate_coaching_output(data):
|
42 |
+
"""
|
43 |
+
Generate daily checklist and tips using Hugging Face LLM.
|
44 |
+
"""
|
45 |
+
logger.info("Generating coaching output for supervisor %s", data['supervisor_id'])
|
46 |
+
milestones_json = json.dumps(data['milestones'], indent=2)
|
47 |
+
prompt = f"""
|
48 |
+
You are an AI Coach for construction site supervisors. Based on the following data, generate a daily checklist, three focus tips, and a motivational quote. Ensure outputs are concise, actionable, and tailored to the supervisor's role, project status, and reflection log.
|
49 |
+
|
50 |
+
Supervisor Role: {data['role']}
|
51 |
+
Project Milestones: {milestones_json}
|
52 |
+
Reflection Log: {data['reflection_log']}
|
53 |
+
Weather: {data['weather']}
|
54 |
+
|
55 |
+
Format the response as JSON:
|
56 |
+
{{
|
57 |
+
"checklist": ["item1", "item2", ...],
|
58 |
+
"tips": ["tip1", "tip2", "tip3"],
|
59 |
+
"quote": "motivational quote"
|
60 |
+
}}
|
61 |
+
"""
|
62 |
+
|
63 |
+
headers = {
|
64 |
+
"Authorization": f"Bearer {HUGGING_FACE_API_TOKEN}",
|
65 |
+
"Content-Type": "application/json"
|
66 |
+
}
|
67 |
+
payload = {
|
68 |
+
"inputs": prompt,
|
69 |
+
"parameters": {
|
70 |
+
"max_length": 200,
|
71 |
+
"temperature": 0.7,
|
72 |
+
"top_p": 0.9
|
73 |
+
}
|
74 |
+
}
|
75 |
|
|
|
|
|
76 |
try:
|
77 |
+
response = requests.post(HUGGING_FACE_API_URL, headers=headers, json=payload, timeout=5)
|
78 |
+
response.raise_for_status()
|
79 |
+
result = response.json()
|
80 |
+
generated_text = result[0]["generated_text"] if isinstance(result, list) else result["generated_text"]
|
81 |
+
|
82 |
+
start_idx = generated_text.find('{')
|
83 |
+
end_idx = generated_text.rfind('}') + 1
|
84 |
+
if start_idx == -1 or end_idx == 0:
|
85 |
+
logger.error("No valid JSON found in LLM output")
|
86 |
+
raise ValueError("No valid JSON found in LLM output")
|
87 |
|
88 |
+
json_str = generated_text[start_idx:end_idx]
|
89 |
+
output = json.loads(json_str)
|
90 |
+
logger.info("Successfully generated coaching output")
|
91 |
+
return output
|
92 |
+
|
93 |
+
except requests.exceptions.HTTPError as e:
|
94 |
+
logger.error("Hugging Face API HTTP error: %s", e)
|
95 |
+
return None
|
96 |
+
except (json.JSONDecodeError, ValueError) as e:
|
97 |
+
logger.error("Error parsing LLM output: %s", e)
|
98 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
except Exception as e:
|
100 |
+
logger.error("Unexpected error in Hugging Face API call: %s", e)
|
101 |
+
return None
|
102 |
+
|
103 |
+
def save_to_salesforce(output, supervisor_id, project_id):
|
104 |
+
"""
|
105 |
+
Save coaching output to Salesforce Supervisor_AI_Coaching__c object.
|
106 |
+
"""
|
107 |
+
if not output:
|
108 |
+
logger.error("No coaching output to save")
|
109 |
return False
|
110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
111 |
try:
|
112 |
+
sf = Salesforce(
|
113 |
+
username=SALESFORCE_USERNAME,
|
114 |
+
password=SALESFORCE_PASSWORD,
|
115 |
+
security_token=SALESFORCE_SECURITY_TOKEN,
|
116 |
+
domain=SALESFORCE_DOMAIN
|
117 |
+
)
|
118 |
+
logger.info("Connected to Salesforce")
|
119 |
+
|
120 |
+
coaching_record = {
|
121 |
+
"Supervisor_ID__c": supervisor_id,
|
122 |
+
"Project_ID__c": project_id,
|
123 |
+
"Daily_Checklist__c": "\n".join(output["checklist"]),
|
124 |
+
"Suggested_Tips__c": "\n".join(output["tips"]),
|
125 |
+
"Quote__c": output["quote"],
|
126 |
+
"Generated_Date__c": datetime.now().strftime("%Y-%m-%d")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
}
|
|
|
|
|
128 |
|
129 |
+
sf.Supervisor_AI_Coaching__c.upsert(
|
130 |
+
f"Supervisor_ID__c/{supervisor_id}_{datetime.now().strftime('%Y-%m-%d')}",
|
131 |
+
coaching_record
|
132 |
+
)
|
133 |
+
logger.info("Successfully saved coaching record to Salesforce for supervisor %s", supervisor_id)
|
134 |
+
return True
|
135 |
|
|
|
|
|
|
|
136 |
except Exception as e:
|
137 |
+
logger.error("Salesforce error: %s", e)
|
138 |
+
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
|
140 |
+
@app.route('/', methods=['GET'])
|
141 |
+
def redirect_to_ui():
|
142 |
+
"""
|
143 |
+
Redirect root URL to the UI.
|
144 |
+
"""
|
145 |
+
return redirect(url_for('ui'))
|
146 |
+
|
147 |
+
@app.route('/ui', methods=['GET'])
|
148 |
+
def ui():
|
149 |
+
"""
|
150 |
+
Serve the HTML user interface.
|
151 |
+
"""
|
152 |
+
return render_template('index.html')
|
153 |
|
154 |
+
@app.route('/generate', methods=['POST'])
|
155 |
+
def generate_endpoint():
|
156 |
+
"""
|
157 |
+
Endpoint to generate coaching output based on supervisor data.
|
158 |
+
"""
|
159 |
try:
|
160 |
+
data = request.get_json()
|
161 |
+
if not data or not all(key in data for key in ['supervisor_id', 'role', 'project_id', 'milestones', 'reflection_log', 'weather']):
|
162 |
+
return jsonify({"status": "error", "message": "Invalid or missing supervisor data"}), 400
|
163 |
+
|
164 |
+
coaching_output = generate_coaching_output(data)
|
165 |
+
if coaching_output:
|
166 |
+
success = save_to_salesforce(coaching_output, data["supervisor_id"], data["project_id"])
|
167 |
+
if success:
|
168 |
+
return jsonify({"status": "success", "output": coaching_output}), 200
|
169 |
+
else:
|
170 |
+
return jsonify({"status": "error", "message": "Failed to save to Salesforce"}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
else:
|
172 |
+
return jsonify({"status": "error", "message": "Failed to generate coaching output"}), 500
|
|
|
173 |
except Exception as e:
|
174 |
+
logger.error("Error in generate endpoint: %s", e)
|
175 |
return jsonify({"status": "error", "message": str(e)}), 500
|
176 |
|
177 |
+
@app.route('/health', methods=['GET'])
|
178 |
+
def health_check():
|
179 |
+
"""
|
180 |
+
Health check endpoint.
|
181 |
+
"""
|
182 |
+
return jsonify({"status": "healthy", "message": "Application is running"}), 200
|
183 |
+
|
184 |
+
if __name__ == "__main__":
|
185 |
+
app.run(host="0.0.0.0", port=int(os.getenv("PORT", 7860)))
|