import os import logging import time import random from datetime import timedelta import secrets from functools import wraps from flask import Flask, render_template, request, jsonify, session, redirect, url_for from flask_cors import CORS from flask_limiter import Limiter from flask_limiter.util import get_remote_address from flask_wtf.csrf import CSRFProtect from openai import OpenAI from scripts.database import DatabaseManager from scripts.rag_helper import RAGHelper logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = Flask(__name__) # ------------------------------------------------------------ # Critical session cookie settings for HTTPS (Hugging Face Spaces) # ------------------------------------------------------------ app.config.update( SECRET_KEY=os.environ.get('SECRET_KEY', secrets.token_urlsafe(32)), SESSION_COOKIE_SECURE=True, # Required for HTTPS SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SAMESITE='None', # Allow cross-site (needed for Spaces) PERMANENT_SESSION_LIFETIME=timedelta(hours=24), SESSION_COOKIE_NAME='tmc_session', WTF_CSRF_CHECK_DEFAULT=False # Disable global CSRF (we use per‑route exemption) ) # CORS with credentials support – allow your Space domain or '*' for testing CORS(app, supports_credentials=True, origins=["https://moderator404-chatbot.hf.space"]) csrf = CSRFProtect(app) limiter = Limiter(key_func=get_remote_address, app=app, default_limits=["1000 per hour", "100 per minute"]) db = DatabaseManager() rag_helper = RAGHelper(use_vector_search=True) # ---------- Hugging Face Inference Providers (OpenAI-compatible) ---------- HF_TOKEN = os.environ.get('HF_TOKEN') HUGGINGFACE_MODEL = "google/gemma-4-26B-A4B-it:novita" # or any other supported model API_BASE_URL = "https://router.huggingface.co/v1" if HF_TOKEN and HF_TOKEN != "dummy": hf_client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN) else: hf_client = None logger.warning("HF_TOKEN not set. Using mock responses only.") # ---------- Mock response fallback (for when HF API is unavailable) ---------- def mock_response(message, rag_context, ticket_context): msg_lower = message.lower() if rag_context: first_line = rag_context.split('\n')[0] if len(first_line) > 20: return f"Based on our knowledge base: {first_line[:200]}" if any(w in msg_lower for w in ['cable','usb-c','hdmi','lightning']): return "We offer high-quality cables with lifetime warranty. Check our products page for details." if any(w in msg_lower for w in ['return','refund','warranty']): return "30-day money-back guarantee and lifetime warranty on all cables. Contact support for returns." if any(w in msg_lower for w in ['shipping','delivery']): return "Free shipping on orders over $25. Most orders ship same-day." if any(w in msg_lower for w in ['hello','hi','hey']): return "Hello! I'm TMCBot. How can I help you today?" return "I'm here to help with cables, orders, and technical support. Could you provide more details?" # ------------------------------------------------------------------- # ChatBot class # ------------------------------------------------------------------- class ChatBot: def __init__(self, db_manager): self.db_manager = db_manager def get_configured_model(self): return HUGGINGFACE_MODEL def send_message(self, message, conversation_id=None, user_id=None, session_id=None): if conversation_id is None: conversation_id = self.db_manager.create_conversation(user_id=user_id, session_id=session_id) self.db_manager.add_message(conversation_id, 'user', message) # Get RAG and ticket context rag_context = rag_helper.get_relevant_context(message) ticket_context, tickets_found = self.get_controlled_ticket_context(message, user_id) # Build system prompt system_msg = "You are a helpful customer service agent for Too Many Cables. Answer concisely in 1-2 sentences." if rag_context: system_msg += f"\nRelevant info: {rag_context[:400]}" if ticket_context and tickets_found: system_msg += f"\nTicket info: {ticket_context}" bot_response = None api_worked = False # Try Hugging Face API if client available if hf_client: try: completion = hf_client.chat.completions.create( model=HUGGINGFACE_MODEL, messages=[ {"role": "system", "content": system_msg}, {"role": "user", "content": message} ], temperature=0.3, max_tokens=150, top_p=0.9, ) bot_response = completion.choices[0].message.content.strip() if bot_response: api_worked = True logger.info("HF Router API returned a response") else: bot_response = None except Exception as e: logger.warning(f"HF Router API exception: {e}") # Fallback to mock if not api_worked: bot_response = mock_response(message, rag_context, ticket_context) logger.info("Using mock response (API unavailable)") self.db_manager.add_message(conversation_id, 'assistant', bot_response, model_used=HUGGINGFACE_MODEL) return { 'success': True, 'response': bot_response, 'conversation_id': conversation_id, 'response_time_ms': 0, 'rag_used': bool(rag_context), 'rag_context_length': len(rag_context), 'tickets_used': tickets_found } def get_conversation(self, conversation_id): return self.db_manager.get_conversation_history(conversation_id) def clear_conversation(self, conversation_id): with self.db_manager.get_connection() as conn: conn.execute("UPDATE conversations SET is_active = 0 WHERE id = ?", (conversation_id,)) conn.commit() return True def get_controlled_ticket_context(self, message, user_id): import re ticket_matches = re.findall(r'TMC-\d{6}', message.upper()) if not ticket_matches and not any(k in message.lower() for k in ['ticket','tickets']): return None, False if not user_id: return "Please log in to view your tickets.", True conn = self.db_manager.get_connection() cursor = conn.cursor() if ticket_matches: tn = ticket_matches[0] cursor.execute("SELECT ticket_number, status, priority, category, description, created_at FROM support_tickets WHERE ticket_number=? AND user_id=?", (tn, user_id)) t = cursor.fetchone() conn.close() if t: return f"Ticket {t['ticket_number']}: {t['status']}, {t['priority']} priority. Created {t['created_at']}. Description: {t['description']}", True return f"Ticket {tn} not found.", True else: cursor.execute("SELECT ticket_number, status, priority, category, created_at FROM support_tickets WHERE user_id=? AND status!='closed' ORDER BY created_at DESC LIMIT 5", (user_id,)) tickets = cursor.fetchall() conn.close() if not tickets: return "You have no open tickets.", True result = "Your recent tickets:\n" + "\n".join(f"- {t['ticket_number']}: {t['status']} ({t['priority']}) - {t['category']}" for t in tickets) return result, True def get_user_ticket_context(self, user_id): if not user_id: return None conn = self.db_manager.get_connection() cursor = conn.cursor() cursor.execute("SELECT ticket_number, subject, status, priority, category, created_at FROM support_tickets WHERE user_id=? AND status!='closed' ORDER BY created_at DESC", (user_id,)) tickets = cursor.fetchall() conn.close() return {"tickets": [dict(t) for t in tickets], "user_name": "Customer"} def create_ticket_from_chat(self, user_id, subject, description, category, priority, conversation_id): return self.db_manager.create_support_ticket(user_id, subject, description, category, conversation_id, priority) def add_conversation_summary_to_tickets(self, conversation_id): logger.info(f"Summary for conversation {conversation_id} would be added here.") return True chatbot = ChatBot(db) # ------------------------------------------------------------------- # Authentication helpers # ------------------------------------------------------------------- def is_authenticated(): sid = session.get('session_id') uid = session.get('user_id') if not sid or not uid: return False user = db.get_user_by_session(sid) return user and user['id'] == uid def require_auth(f): @wraps(f) def decorated(*args, **kwargs): if not is_authenticated(): return jsonify({'success': False, 'error': 'Authentication required'}), 401 return f(*args, **kwargs) return decorated def require_role(role): def decorator(f): @wraps(f) def decorated(*args, **kwargs): uid = session.get('user_id') if not uid: return jsonify({'error': 'Auth required'}), 401 user_role = db.get_user_role(uid) if user_role != role: return jsonify({'error': 'Insufficient privileges'}), 403 return f(*args, **kwargs) return decorated return decorator # ------------------------------------------------------------------- # Web Routes # ------------------------------------------------------------------- @app.route('/') def homepage(): return render_template('homepage.html') @app.route('/products') def products(): return render_template('products.html') @app.route('/chat') def chat(): cache_bust = int(time.time()) return render_template('chat.html', configured_model=chatbot.get_configured_model(), cache_bust=cache_bust) @app.route('/tickets') def tickets(): return render_template('tickets.html') @app.route('/admin') def admin(): return redirect(url_for('admin_tickets')) @app.route('/admin/tickets') @require_role('admin') def admin_tickets(): return render_template('admin_tickets.html') # ------------------------------------------------------------------- # API Endpoints (all exempt from CSRF) # ------------------------------------------------------------------- @app.route('/api/chat', methods=['POST']) @csrf.exempt def api_chat(): data = request.get_json() message = data.get('message') conv_id = data.get('conversation_id') if not message: return jsonify({'success': False, 'error': 'Message required'}), 400 result = chatbot.send_message(message, conv_id, session.get('user_id'), session.get('session_id')) return jsonify(result) @app.route('/api/conversation/') def get_conversation(conversation_id): return jsonify({'success': True, 'messages': chatbot.get_conversation(conversation_id)}) @app.route('/api/conversation//clear', methods=['POST']) @csrf.exempt def clear_conversation(conversation_id): return jsonify({'success': chatbot.clear_conversation(conversation_id)}) @app.route('/api/conversation/end', methods=['POST']) @csrf.exempt def end_conversation(): data = request.get_json() conv_id = data.get('conversation_id') if conv_id: chatbot.add_conversation_summary_to_tickets(conv_id) return jsonify({'success': True, 'message': 'Conversation ended'}) @app.route('/api/login', methods=['POST']) @csrf.exempt def login(): data = request.get_json() user = db.authenticate_user(data.get('email'), data.get('password')) if user: sid = db.create_session(user['id'], request.remote_addr, request.headers.get('User-Agent', '')) session['user_id'] = user['id'] session['session_id'] = sid return jsonify({'success': True, 'user': {'id': user['id'], 'email': user['email'], 'name': f"{user['first_name']} {user['last_name']}"}}) return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 @app.route('/api/register', methods=['POST']) @csrf.exempt def register(): data = request.get_json() uid = db.create_user(data['email'], data['first_name'], data['last_name'], data['password'], data.get('phone'), data.get('company')) if uid: return jsonify({'success': True}) return jsonify({'success': False, 'error': 'Email exists'}), 409 @app.route('/api/user') def get_user(): if not is_authenticated(): return jsonify({'authenticated': False}) uid = session['user_id'] conn = db.get_connection() cur = conn.cursor() cur.execute("SELECT id, email, first_name, last_name FROM users WHERE id = ?", (uid,)) u = cur.fetchone() conn.close() if u: return jsonify({'authenticated': True, 'user': {'id': u[0], 'email': u[1], 'name': f"{u[2]} {u[3]}"}}) return jsonify({'authenticated': False}) @app.route('/api/logout', methods=['POST']) @csrf.exempt def logout(): session.clear() return jsonify({'success': True}) @app.route('/api/tickets/create', methods=['POST']) @require_auth @csrf.exempt def create_ticket(): data = request.get_json() tn = db.create_support_ticket( session['user_id'], data['subject'], data['description'], data.get('category', 'General'), data.get('conversation_id'), data.get('priority', 'medium') ) return jsonify({'success': True, 'ticket_number': tn}) @app.route('/api/tickets/user') @require_auth def get_user_tickets(): tickets = db.get_user_tickets(session['user_id']) return jsonify({'success': True, 'tickets': tickets}) @app.route('/api/tickets/') @require_auth def get_ticket(ticket_number): ticket = db.get_ticket_by_number(ticket_number) if not ticket or ticket['user_id'] != session['user_id']: return jsonify({'error': 'Not found'}), 404 updates = db.get_ticket_updates(ticket['id']) return jsonify({'success': True, 'ticket': ticket, 'updates': updates}) @app.route('/api/tickets//update', methods=['POST']) @require_auth @csrf.exempt def add_ticket_update(ticket_id): data = request.get_json() db.add_ticket_update(ticket_id, session['user_id'], data['message'], 'note') return jsonify({'success': True}) @app.route('/api/chat/user-tickets') @require_auth def chat_user_tickets(): ctx = chatbot.get_user_ticket_context(session['user_id']) return jsonify({'success': True, 'tickets': ctx['tickets'] if ctx else []}) @app.route('/api/chat/create-ticket', methods=['POST']) @require_auth @csrf.exempt def chat_create_ticket(): data = request.get_json() tn = chatbot.create_ticket_from_chat( session['user_id'], data['subject'], data['description'], data.get('category', 'General'), data.get('priority', 'medium'), data.get('conversation_id') ) return jsonify({'success': True, 'ticket_number': tn, 'message': f'Ticket {tn} created'}) @app.route('/api/health') def health(): return jsonify({'status': 'healthy', 'model': HUGGINGFACE_MODEL}) @app.route('/api/knowledge-base/stats') def kb_stats(): return jsonify(rag_helper.get_knowledge_base_stats()) @app.route('/api/admin/tickets') @require_role('admin') def admin_get_tickets(): tickets = db.get_tickets_by_status('', limit=100) return jsonify({'success': True, 'tickets': tickets}) @app.route('/api/admin/tickets/stats') @require_role('admin') def admin_ticket_stats(): with db.get_connection() as conn: cur = conn.cursor() cur.execute("SELECT COUNT(*) as total FROM support_tickets") total = cur.fetchone()['total'] cur.execute("SELECT COUNT(*) as open FROM support_tickets WHERE status='open'") open_t = cur.fetchone()['open'] cur.execute("SELECT COUNT(*) as in_progress FROM support_tickets WHERE status='in_progress'") in_prog = cur.fetchone()['in_progress'] cur.execute("SELECT COUNT(*) as resolved FROM support_tickets WHERE status='resolved'") resolved = cur.fetchone()['resolved'] return jsonify({'success': True, 'stats': {'overall': {'total_tickets': total, 'open_tickets': open_t, 'in_progress_tickets': in_prog, 'resolved_tickets': resolved}}}) @app.route('/api/tickets/categories') def ticket_categories(): with db.get_connection() as conn: cur = conn.cursor() cur.execute("SELECT name, description FROM ticket_categories WHERE is_active=1") cats = [dict(row) for row in cur.fetchall()] return jsonify({'success': True, 'categories': cats}) @app.route('/api/admin/tickets//assign', methods=['PUT']) @require_role('admin') @csrf.exempt def admin_assign_ticket(ticket_id): data = request.get_json() agent = data.get('assigned_agent') with db.get_connection() as conn: conn.execute("UPDATE support_tickets SET assigned_agent = ? WHERE id = ?", (agent, ticket_id)) conn.commit() return jsonify({'success': True}) @app.route('/api/admin/tickets//status', methods=['PUT']) @require_role('admin') @csrf.exempt def admin_update_status(ticket_id): data = request.get_json() new_status = data.get('status') notes = data.get('resolution_notes', '') with db.get_connection() as conn: conn.execute("UPDATE support_tickets SET status = ?, resolution_notes = ? WHERE id = ?", (new_status, notes, ticket_id)) conn.commit() return jsonify({'success': True}) @app.route('/api/admin/tickets//reply', methods=['POST']) @require_role('admin') @csrf.exempt def admin_add_reply(ticket_id): data = request.get_json() message = data.get('message') is_internal = data.get('is_internal', False) db.add_ticket_update(ticket_id, session['user_id'], message, 'admin_reply', is_internal) return jsonify({'success': True}) @app.route('/api/product/') def get_product_specs(product_name): import os path = f"knowledge_base/product_manuals/{product_name}.md" if os.path.exists(path): with open(path, 'r', encoding='utf-8') as f: return jsonify({'success': True, 'specifications': f.read()}) return jsonify({'success': False, 'error': 'Product not found'}), 404 # ------------------------------------------------------------------- # Debug endpoint (optional) # ------------------------------------------------------------------- @app.route('/api/debug-headers') def debug_headers(): return jsonify(dict(request.headers)) if __name__ == '__main__': app.run(host='0.0.0.0', port=7860, debug=False)