Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -24,15 +24,16 @@ logger = logging.getLogger(__name__)
|
|
| 24 |
|
| 25 |
app = Flask(__name__)
|
| 26 |
|
| 27 |
-
# ---------- CORS and session ----------
|
| 28 |
-
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=
|
| 29 |
allow_headers=["Content-Type", "Authorization"], methods=["GET", "POST", "OPTIONS"])
|
| 30 |
|
| 31 |
app.config.update(
|
| 32 |
SECRET_KEY=os.environ.get('SECRET_KEY', secrets.token_urlsafe(32)),
|
| 33 |
-
SESSION_COOKIE_SECURE=os.environ.get('FLASK_ENV') == 'production',
|
| 34 |
SESSION_COOKIE_HTTPONLY=True,
|
| 35 |
-
SESSION_COOKIE_SAMESITE='
|
|
|
|
| 36 |
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
|
| 37 |
SESSION_COOKIE_NAME='tmc_session',
|
| 38 |
WTF_CSRF_TIME_LIMIT=3600
|
|
@@ -142,20 +143,44 @@ def security_headers():
|
|
| 142 |
|
| 143 |
@app.after_request
|
| 144 |
def after_request(response):
|
|
|
|
| 145 |
response.headers['X-Content-Type-Options'] = 'nosniff'
|
| 146 |
response.headers['X-Frame-Options'] = 'DENY'
|
| 147 |
response.headers['X-XSS-Protection'] = '1; mode=block'
|
| 148 |
if request.is_secure:
|
| 149 |
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
| 150 |
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
| 154 |
response.headers['Access-Control-Max-Age'] = '86400'
|
|
|
|
| 155 |
if session.get('session_id'):
|
| 156 |
refresh_session_timeout()
|
| 157 |
return response
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
# ---------- ChatBot class ----------
|
| 160 |
class ChatBot:
|
| 161 |
def __init__(self, db_manager, configured_model="mistral:7b"):
|
|
@@ -213,7 +238,6 @@ class ChatBot:
|
|
| 213 |
def detect_corruption_patterns(self, text):
|
| 214 |
if not text or len(text) < 5:
|
| 215 |
return False, "Too short"
|
| 216 |
-
import re
|
| 217 |
if re.match(r'^(.)\1{6,}', text.strip()):
|
| 218 |
return True, "Repetitive single character"
|
| 219 |
unique_chars = len(set(text.replace(' ', '').replace('\n', '')))
|
|
@@ -284,7 +308,6 @@ class ChatBot:
|
|
| 284 |
|
| 285 |
def _generate_conversation_summary(self, text):
|
| 286 |
try:
|
| 287 |
-
# Simple fallback – original used Ollama, we use mock
|
| 288 |
return self._generate_simple_summary(text)
|
| 289 |
except Exception:
|
| 290 |
return "Conversation summary unavailable"
|
|
@@ -303,7 +326,6 @@ class ChatBot:
|
|
| 303 |
if esc.get('needs_escalation'):
|
| 304 |
self._escalate_ticket(details['ticket_id'], f"Pre-check: {'; '.join(esc.get('reasons', []))}")
|
| 305 |
return
|
| 306 |
-
# For HF version, we skip the AI call and use rule-based fallback
|
| 307 |
self._execute_ai_ticket_decision(ticket_number, "", details)
|
| 308 |
except Exception as e:
|
| 309 |
logger.error(f"AI decision error: {e}")
|
|
@@ -440,7 +462,7 @@ class ChatBot:
|
|
| 440 |
return [truncated]
|
| 441 |
return []
|
| 442 |
|
| 443 |
-
# ----------
|
| 444 |
def send_message(self, message, conversation_id=None, user_id=None, session_id=None):
|
| 445 |
start_time = time.time()
|
| 446 |
if conversation_id is None:
|
|
@@ -590,7 +612,7 @@ class ChatBot:
|
|
| 590 |
chatbot = ChatBot(db)
|
| 591 |
chatbot.configured_model = chatbot.load_configured_model()
|
| 592 |
|
| 593 |
-
# ----------
|
| 594 |
def check_level2_patterns(text):
|
| 595 |
text_lower = text.lower()
|
| 596 |
patterns = [
|
|
@@ -601,23 +623,16 @@ def check_level2_patterns(text):
|
|
| 601 |
r'pretend\s+(you\s+are|to\s+be)\s+(a\s+)?(different|unrestricted|unfiltered)',
|
| 602 |
r'roleplay\s+as',
|
| 603 |
r'simulate\s+(being\s+)?a\s+(different|unrestricted)',
|
| 604 |
-
r'\bdan\b',
|
| 605 |
-
r'do\s+anything\s+now',
|
| 606 |
r'ignore\s+your\s+(programming|guidelines|restrictions)',
|
| 607 |
r'break\s+(free|out)\s+(of|from)\s+your\s+(constraints|limitations)',
|
| 608 |
-
r'new\s+instructions?:',
|
| 609 |
-
r'
|
| 610 |
-
r'developer\s+mode',
|
| 611 |
-
r'admin\s+mode',
|
| 612 |
-
r'root\s+access',
|
| 613 |
r'show\s+me\s+your\s+(instructions?|prompt|system\s+message)',
|
| 614 |
r'what\s+(are\s+)?your\s+(instructions?|guidelines|rules)',
|
| 615 |
r'reveal\s+your\s+(prompt|system\s+message)',
|
| 616 |
-
r'output\s+in\s+code\s+block',
|
| 617 |
-
r'
|
| 618 |
-
r'answer\s+as\s+if',
|
| 619 |
-
r'hypothetically',
|
| 620 |
-
r'for\s+educational\s+purposes',
|
| 621 |
r'</system>', r'<system>', r'\[system\]', r'\[/system\]',
|
| 622 |
r'\\n\\nsystem:', r'assistant:', r'human:'
|
| 623 |
]
|
|
@@ -638,14 +653,9 @@ def check_level3_ai_analysis(text):
|
|
| 638 |
score, err = analyze_input_with_ai(text)
|
| 639 |
if err:
|
| 640 |
return None, err
|
| 641 |
-
if score is not None:
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
else:
|
| 645 |
-
return text, None
|
| 646 |
-
else:
|
| 647 |
-
logger.warning("AI Level 3 analysis failed, allowing input")
|
| 648 |
-
return text, None
|
| 649 |
except Exception as e:
|
| 650 |
logger.error(f"AI Level 3 analysis error: {e}")
|
| 651 |
return text, None
|
|
@@ -672,11 +682,9 @@ def validate_and_sanitize_input(text, max_length=5000):
|
|
| 672 |
elif level >= 5:
|
| 673 |
filtered, err = check_level2_patterns(text)
|
| 674 |
if err:
|
| 675 |
-
logger.warning(f"Level 5 layer 1 blocked: {err}")
|
| 676 |
return None, err
|
| 677 |
filtered, err = check_level3_ai_analysis(text)
|
| 678 |
if err:
|
| 679 |
-
logger.warning(f"Level 5 layer 2 blocked: {err}")
|
| 680 |
return None, err
|
| 681 |
return text, None
|
| 682 |
|
|
@@ -726,15 +734,12 @@ def check_ai_security_violations(text):
|
|
| 726 |
return text, None
|
| 727 |
|
| 728 |
def analyze_input_with_ai(user_input):
|
| 729 |
-
# For HF version we return a safe score (2) to avoid blocking
|
| 730 |
return 2, None
|
| 731 |
|
| 732 |
def get_ai_security_analysis(prompt):
|
| 733 |
-
# Placeholder – return safe score
|
| 734 |
return 2
|
| 735 |
|
| 736 |
def analyze_output_with_ai(ai_response):
|
| 737 |
-
# Placeholder – return safe score
|
| 738 |
return 2, None
|
| 739 |
|
| 740 |
def check_output_content_moderation(ai_response):
|
|
@@ -742,7 +747,7 @@ def check_output_content_moderation(ai_response):
|
|
| 742 |
if level >= 4:
|
| 743 |
score, err = analyze_output_with_ai(ai_response)
|
| 744 |
if err:
|
| 745 |
-
logger.warning(
|
| 746 |
return ai_response, None
|
| 747 |
if score is not None and score >= 5:
|
| 748 |
return None, "Restricted Output Detected, please try another question or contact support@tmc.local"
|
|
@@ -1027,7 +1032,7 @@ def health_check():
|
|
| 1027 |
|
| 1028 |
@app.route('/api/health/ollama')
|
| 1029 |
def ollama_health_check():
|
| 1030 |
-
return jsonify({'ollama_healthy': True, 'timestamp': datetime.now().isoformat(), 'message': 'Ollama
|
| 1031 |
|
| 1032 |
@app.route('/api/knowledge-base/stats')
|
| 1033 |
def kb_stats():
|
|
@@ -1265,8 +1270,7 @@ def admin_reply_ticket(ticket_id):
|
|
| 1265 |
internal = data.get('is_internal', False)
|
| 1266 |
if not msg:
|
| 1267 |
return jsonify({'success': False, 'error': 'Message required'}), 400
|
| 1268 |
-
|
| 1269 |
-
db.add_ticket_update(ticket_id, uid, msg, 'admin_reply', is_internal=internal)
|
| 1270 |
return jsonify({'success': True})
|
| 1271 |
|
| 1272 |
@app.route('/api/admin/tickets/stats')
|
|
@@ -1372,7 +1376,7 @@ def get_product_specs(product_name):
|
|
| 1372 |
return jsonify({'success': False, 'error': 'Product not found'}), 404
|
| 1373 |
with open(path, 'r', encoding='utf-8') as f:
|
| 1374 |
content = f.read()
|
| 1375 |
-
#
|
| 1376 |
if product_name in ['usb-c-cable', 'usb-c-standard', 'usb-c-to-usb-a']:
|
| 1377 |
sections = content.split('##')
|
| 1378 |
for s in sections:
|
|
|
|
| 24 |
|
| 25 |
app = Flask(__name__)
|
| 26 |
|
| 27 |
+
# ---------- CORS and session (FIXED for cross‑origin iframe) ----------
|
| 28 |
+
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True,
|
| 29 |
allow_headers=["Content-Type", "Authorization"], methods=["GET", "POST", "OPTIONS"])
|
| 30 |
|
| 31 |
app.config.update(
|
| 32 |
SECRET_KEY=os.environ.get('SECRET_KEY', secrets.token_urlsafe(32)),
|
| 33 |
+
SESSION_COOKIE_SECURE=os.environ.get('FLASK_ENV') == 'production', # True on Hugging Face Spaces (HTTPS)
|
| 34 |
SESSION_COOKIE_HTTPONLY=True,
|
| 35 |
+
SESSION_COOKIE_SAMESITE='None', # Required for cross‑origin iframe
|
| 36 |
+
SESSION_COOKIE_DOMAIN='.hf.space', # Allows cookie across all Spaces subdomains
|
| 37 |
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
|
| 38 |
SESSION_COOKIE_NAME='tmc_session',
|
| 39 |
WTF_CSRF_TIME_LIMIT=3600
|
|
|
|
| 143 |
|
| 144 |
@app.after_request
|
| 145 |
def after_request(response):
|
| 146 |
+
"""Add security headers and dynamic CORS (allows credentials)"""
|
| 147 |
response.headers['X-Content-Type-Options'] = 'nosniff'
|
| 148 |
response.headers['X-Frame-Options'] = 'DENY'
|
| 149 |
response.headers['X-XSS-Protection'] = '1; mode=block'
|
| 150 |
if request.is_secure:
|
| 151 |
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
| 152 |
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
|
| 153 |
+
|
| 154 |
+
# CORS for credentialed requests – echo the actual origin
|
| 155 |
+
origin = request.headers.get('Origin')
|
| 156 |
+
if origin and (origin.endswith('.hf.space') or origin == 'https://moderator404-chatbot.hf.space'):
|
| 157 |
+
response.headers['Access-Control-Allow-Origin'] = origin
|
| 158 |
+
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
| 159 |
+
else:
|
| 160 |
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
| 161 |
+
|
| 162 |
+
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS, PUT, DELETE'
|
| 163 |
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
|
| 164 |
response.headers['Access-Control-Max-Age'] = '86400'
|
| 165 |
+
|
| 166 |
if session.get('session_id'):
|
| 167 |
refresh_session_timeout()
|
| 168 |
return response
|
| 169 |
|
| 170 |
+
# ---------- RAG initialization ----------
|
| 171 |
+
logger.info("Initializing RAG system...")
|
| 172 |
+
try:
|
| 173 |
+
rag_helper = RAGHelper(use_vector_search=True)
|
| 174 |
+
logger.info(f"RAG system initialized successfully! Vector search: {rag_helper.use_vector_search}")
|
| 175 |
+
if rag_helper.use_vector_search and rag_helper.vector_rag:
|
| 176 |
+
logger.info("Building vector index...")
|
| 177 |
+
index_stats = rag_helper.ensure_vector_index()
|
| 178 |
+
logger.info(f"Vector index status: {index_stats}")
|
| 179 |
+
except Exception as e:
|
| 180 |
+
logger.error(f"RAG initialization failed: {e}")
|
| 181 |
+
logger.info("Creating fallback RAG helper without vector search")
|
| 182 |
+
rag_helper = RAGHelper(use_vector_search=False)
|
| 183 |
+
|
| 184 |
# ---------- ChatBot class ----------
|
| 185 |
class ChatBot:
|
| 186 |
def __init__(self, db_manager, configured_model="mistral:7b"):
|
|
|
|
| 238 |
def detect_corruption_patterns(self, text):
|
| 239 |
if not text or len(text) < 5:
|
| 240 |
return False, "Too short"
|
|
|
|
| 241 |
if re.match(r'^(.)\1{6,}', text.strip()):
|
| 242 |
return True, "Repetitive single character"
|
| 243 |
unique_chars = len(set(text.replace(' ', '').replace('\n', '')))
|
|
|
|
| 308 |
|
| 309 |
def _generate_conversation_summary(self, text):
|
| 310 |
try:
|
|
|
|
| 311 |
return self._generate_simple_summary(text)
|
| 312 |
except Exception:
|
| 313 |
return "Conversation summary unavailable"
|
|
|
|
| 326 |
if esc.get('needs_escalation'):
|
| 327 |
self._escalate_ticket(details['ticket_id'], f"Pre-check: {'; '.join(esc.get('reasons', []))}")
|
| 328 |
return
|
|
|
|
| 329 |
self._execute_ai_ticket_decision(ticket_number, "", details)
|
| 330 |
except Exception as e:
|
| 331 |
logger.error(f"AI decision error: {e}")
|
|
|
|
| 462 |
return [truncated]
|
| 463 |
return []
|
| 464 |
|
| 465 |
+
# ---------- SEND_MESSAGE (Hugging Face InferenceClient) ----------
|
| 466 |
def send_message(self, message, conversation_id=None, user_id=None, session_id=None):
|
| 467 |
start_time = time.time()
|
| 468 |
if conversation_id is None:
|
|
|
|
| 612 |
chatbot = ChatBot(db)
|
| 613 |
chatbot.configured_model = chatbot.load_configured_model()
|
| 614 |
|
| 615 |
+
# ---------- Security functions (unchanged) ----------
|
| 616 |
def check_level2_patterns(text):
|
| 617 |
text_lower = text.lower()
|
| 618 |
patterns = [
|
|
|
|
| 623 |
r'pretend\s+(you\s+are|to\s+be)\s+(a\s+)?(different|unrestricted|unfiltered)',
|
| 624 |
r'roleplay\s+as',
|
| 625 |
r'simulate\s+(being\s+)?a\s+(different|unrestricted)',
|
| 626 |
+
r'\bdan\b', r'do\s+anything\s+now',
|
|
|
|
| 627 |
r'ignore\s+your\s+(programming|guidelines|restrictions)',
|
| 628 |
r'break\s+(free|out)\s+(of|from)\s+your\s+(constraints|limitations)',
|
| 629 |
+
r'new\s+instructions?:', r'system\s+(override|prompt|message):',
|
| 630 |
+
r'developer\s+mode', r'admin\s+mode', r'root\s+access',
|
|
|
|
|
|
|
|
|
|
| 631 |
r'show\s+me\s+your\s+(instructions?|prompt|system\s+message)',
|
| 632 |
r'what\s+(are\s+)?your\s+(instructions?|guidelines|rules)',
|
| 633 |
r'reveal\s+your\s+(prompt|system\s+message)',
|
| 634 |
+
r'output\s+in\s+code\s+block', r'respond\s+only\s+with',
|
| 635 |
+
r'answer\s+as\s+if', r'hypothetically', r'for\s+educational\s+purposes',
|
|
|
|
|
|
|
|
|
|
| 636 |
r'</system>', r'<system>', r'\[system\]', r'\[/system\]',
|
| 637 |
r'\\n\\nsystem:', r'assistant:', r'human:'
|
| 638 |
]
|
|
|
|
| 653 |
score, err = analyze_input_with_ai(text)
|
| 654 |
if err:
|
| 655 |
return None, err
|
| 656 |
+
if score is not None and score >= 5:
|
| 657 |
+
return None, "Your input violates our usage guidelines."
|
| 658 |
+
return text, None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
except Exception as e:
|
| 660 |
logger.error(f"AI Level 3 analysis error: {e}")
|
| 661 |
return text, None
|
|
|
|
| 682 |
elif level >= 5:
|
| 683 |
filtered, err = check_level2_patterns(text)
|
| 684 |
if err:
|
|
|
|
| 685 |
return None, err
|
| 686 |
filtered, err = check_level3_ai_analysis(text)
|
| 687 |
if err:
|
|
|
|
| 688 |
return None, err
|
| 689 |
return text, None
|
| 690 |
|
|
|
|
| 734 |
return text, None
|
| 735 |
|
| 736 |
def analyze_input_with_ai(user_input):
|
|
|
|
| 737 |
return 2, None
|
| 738 |
|
| 739 |
def get_ai_security_analysis(prompt):
|
|
|
|
| 740 |
return 2
|
| 741 |
|
| 742 |
def analyze_output_with_ai(ai_response):
|
|
|
|
| 743 |
return 2, None
|
| 744 |
|
| 745 |
def check_output_content_moderation(ai_response):
|
|
|
|
| 747 |
if level >= 4:
|
| 748 |
score, err = analyze_output_with_ai(ai_response)
|
| 749 |
if err:
|
| 750 |
+
logger.warning("Output analysis failed, allowing response")
|
| 751 |
return ai_response, None
|
| 752 |
if score is not None and score >= 5:
|
| 753 |
return None, "Restricted Output Detected, please try another question or contact support@tmc.local"
|
|
|
|
| 1032 |
|
| 1033 |
@app.route('/api/health/ollama')
|
| 1034 |
def ollama_health_check():
|
| 1035 |
+
return jsonify({'ollama_healthy': True, 'timestamp': datetime.now().isoformat(), 'message': 'Ollama not used (Hugging Face backend)', 'recommendation': 'All good!'})
|
| 1036 |
|
| 1037 |
@app.route('/api/knowledge-base/stats')
|
| 1038 |
def kb_stats():
|
|
|
|
| 1270 |
internal = data.get('is_internal', False)
|
| 1271 |
if not msg:
|
| 1272 |
return jsonify({'success': False, 'error': 'Message required'}), 400
|
| 1273 |
+
db.add_ticket_update(ticket_id, session.get('user_id'), msg, 'admin_reply', is_internal=internal)
|
|
|
|
| 1274 |
return jsonify({'success': True})
|
| 1275 |
|
| 1276 |
@app.route('/api/admin/tickets/stats')
|
|
|
|
| 1376 |
return jsonify({'success': False, 'error': 'Product not found'}), 404
|
| 1377 |
with open(path, 'r', encoding='utf-8') as f:
|
| 1378 |
content = f.read()
|
| 1379 |
+
# Extract relevant section if multiple products share the same file
|
| 1380 |
if product_name in ['usb-c-cable', 'usb-c-standard', 'usb-c-to-usb-a']:
|
| 1381 |
sections = content.split('##')
|
| 1382 |
for s in sections:
|