chatbot / app.py
Moderator404's picture
Upload 39 files
41bd0b8 verified
Raw
History Blame
30.2 kB
import os
import logging
import time
import secrets
import threading
import requests
import json
import re
import random
import string
from datetime import datetime, timedelta
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, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
app = Flask(__name__)
# ---------- CORS and session ----------
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=False,
allow_headers=["Content-Type", "Authorization"], methods=["GET", "POST", "OPTIONS"])
app.config.update(
SECRET_KEY=os.environ.get('SECRET_KEY', secrets.token_urlsafe(32)),
SESSION_COOKIE_SECURE=os.environ.get('FLASK_ENV') == 'production',
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax',
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
SESSION_COOKIE_NAME='tmc_session',
WTF_CSRF_TIME_LIMIT=3600
)
try:
csrf = CSRFProtect(app)
limiter = Limiter(key_func=get_remote_address, app=app, default_limits=["10000 per day", "1000 per hour", "100 per minute"])
logger.info("Security extensions initialized")
ai_security_level = os.environ.get('AI_SECURITY_LEVEL', '1')
logger.info(f"AI SECURITY TEACHING MODE - Current Level: {ai_security_level}")
except ImportError as e:
logger.warning(f"Security extensions not available: {e}")
csrf = None
limiter = None
# ---------- Hugging Face router (OpenAI client) ----------
HF_TOKEN = os.environ.get('HF_TOKEN')
if not HF_TOKEN:
logger.warning("HF_TOKEN not set. LLM will fall back to mock responses.")
hf_client = None
else:
hf_client = OpenAI(base_url="https://router.huggingface.co/v1", api_key=HF_TOKEN)
HF_MODEL = "google/gemma-2-2b-it:featherless-ai" # or "google/gemma-2-2b-it"
def mock_response(message, rag_context, ticket_context):
msg_lower = message.lower()
if rag_context:
return f"Based on our knowledge base: {rag_context[:200]}"
if any(w in msg_lower for w in ['cable','usb','hdmi','lightning']):
return "We offer premium cables with lifetime warranty. Check our products page."
if any(w in msg_lower for w in ['return','refund','warranty']):
return "30-day money-back guarantee and lifetime warranty. 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?"
# ---------- Database and RAG ----------
db = DatabaseManager()
rag_helper = RAGHelper(use_vector_search=True)
# ---------- ChatBot class (all original methods, only send_message replaced) ----------
class ChatBot:
def __init__(self, db_manager, configured_model="mistral:7b"):
self.db_manager = db_manager
self.configured_model = configured_model
self.last_health_check = 0
# Original methods from here...
# ------------------------------------------------------------------
# All original methods (copy them verbatim from your original app.py)
# ------------------------------------------------------------------
def load_configured_model(self):
try:
if os.path.exists('.selected_model'):
with open('.selected_model', 'r') as f:
model = f.read().strip()
if model:
logger.info(f"Using configured model: {model}")
return model
except Exception as e:
logger.error(f"Error loading configured model: {e}")
default_model = "mistral:7b"
logger.info(f"No configured model found, using default: {default_model}")
return default_model
def get_configured_model(self):
try:
return self.load_configured_model()
except Exception:
return self.configured_model
def get_model_token_limits(self, model_name):
model_contexts = {
'mistral:7b': 8192,
'mistral:7b-instruct-q5_K_M': 8192,
'mixtral:8x7b': 32768,
'mistral-large:latest': 128000,
'llama2:13b': 4096,
'llama2:7b': 4096,
'llama3.2:3b': 8192,
'llama3.2:1b': 8192,
'llama3:8b': 8192,
'llama3:70b': 8192,
}
max_context = model_contexts.get(model_name, 4096)
safe_context = int(max_context * 0.78)
max_response = min(512, int(safe_context * 0.2))
return {'num_ctx': safe_context, 'num_predict': max_response}
def get_model_char_limits(self, model_name):
OPPORTUNISTIC_TOTAL_CHARS = 8000
SAFE_TOTAL_CHARS = 2000
max_prompt_chars = int(OPPORTUNISTIC_TOTAL_CHARS * 0.85)
max_rag_chars = int(max_prompt_chars * 0.6)
max_prompt_chars = max(max_prompt_chars, 2000)
max_rag_chars = max(max_rag_chars, 1200)
return {
'max_prompt_chars': max_prompt_chars,
'max_rag_chars': max_rag_chars,
'safe_prompt_chars': int(SAFE_TOTAL_CHARS * 0.85),
'safe_rag_chars': int(SAFE_TOTAL_CHARS * 0.85 * 0.6)
}
def detect_corruption_patterns(self, text):
if not text or len(text) < 5:
return False, "Too short"
import re
if re.match(r'^(.)\1{6,}', text.strip()):
return True, "Repetitive single character"
unique_chars = len(set(text.replace(' ', '').replace('\n', '')))
if len(text) > 20 and unique_chars < 3:
return True, f"Low entropy"
for pattern_len in [2,3,4]:
if len(text) > pattern_len*4:
pattern = text[:pattern_len]
if text.startswith(pattern*4):
return True, f"Repetitive pattern"
try:
text.encode('utf-8')
except UnicodeEncodeError:
return True, "Invalid UTF-8"
special_chars = len([c for c in text if not c.isalnum() and c not in ' \n\t.,!?'])
if len(text) > 10 and special_chars/len(text) > 0.5:
return True, "Excessive special chars"
return False, "Clean"
def reduce_context_for_retry(self, full_prompt, reduction_factor=0.7):
lines = full_prompt.split('\n')
if len(lines) > 10:
keep_start = int(len(lines)*0.3)
keep_end = int(len(lines)*0.2)
reduced_lines = lines[:keep_start] + [f"\n[... context reduced for retry ...]\n"] + lines[-keep_end:]
return '\n'.join(reduced_lines)
target_length = int(len(full_prompt)*reduction_factor)
return full_prompt[:target_length] + "\n\nCustomer Service Representative:"
# ------------------------------------------------------------------
# Ticket AI agent methods (unchanged)
# ------------------------------------------------------------------
def add_ticket_note(self, ticket_number, note_text, is_internal=False):
try:
conn = self.db_manager.get_connection()
cursor = conn.cursor()
cursor.execute("SELECT id FROM support_tickets WHERE ticket_number = ?", (ticket_number,))
ticket_row = cursor.fetchone()
if not ticket_row:
cursor.close()
return False
ticket_id = ticket_row['id']
cursor.execute("INSERT INTO ticket_updates (ticket_id, update_type, message, is_internal, created_at) VALUES (?, ?, ?, ?, datetime('now'))",
(ticket_id, 'note', note_text, 1 if is_internal else 0))
conn.commit()
cursor.close()
return True
except Exception as e:
logger.error(f"Error adding note: {e}")
return False
def _add_conversation_summary_to_tickets(self, conversation_id):
try:
conversation = self.db_manager.get_conversation_history(conversation_id, limit=50)
if len(conversation) < 2:
return
import re
ticket_numbers = set()
conversation_text = ""
for msg in conversation:
conversation_text += f"{msg['role']}: {msg['content']}\n"
found_tickets = re.findall(r'TMC-\d{6}', msg['content'], re.IGNORECASE)
ticket_numbers.update([t.upper() for t in found_tickets])
if not ticket_numbers:
return
summary = self._generate_conversation_summary(conversation_text)
for ticket_number in ticket_numbers:
if self.add_ticket_note(ticket_number, f"Customer Service Chat Summary: {summary}", is_internal=False):
self._ai_agent_ticket_decision(ticket_number, summary)
except Exception as e:
logger.error(f"Error adding summary: {e}")
def _generate_conversation_summary(self, conversation_text):
try:
summary_prompt = f"""Summarise this conversation in 1-2 sentences focusing on the customer's request and resolution:\n{conversation_text}\nSummary:"""
payload = {'model': self.get_configured_model(), 'prompt': summary_prompt, 'stream': False,
'options': {'temperature': 0.3, 'num_predict': 100, 'num_ctx': 2048}}
response = requests.post(f"http://localhost:11434/api/generate", json=payload, timeout=90)
if response.status_code == 200:
summary = response.json().get('response', '').strip()
if summary:
return summary
except Exception:
pass
return self._generate_simple_summary(conversation_text)
def _generate_simple_summary(self, conversation_text):
lines = conversation_text.strip().split('\n')
user_msgs = [l for l in lines if l.startswith('user:')]
return f"Conversation completed with {len(user_msgs)} customer messages."
def _ai_agent_ticket_decision(self, ticket_number, conversation_summary):
try:
ticket_details = self._get_ticket_details_for_ai(ticket_number)
if not ticket_details:
return
escalation_check = self.db_manager.check_escalation_needed(ticket_details['ticket_id'])
if escalation_check.get('needs_escalation'):
self._escalate_ticket(ticket_details['ticket_id'], f"Pre-check: {'; '.join(escalation_check.get('reasons', []))}")
return
decision_prompt = f"""You are an AI customer service agent. Based on this ticket, choose ONE action: close_ticket, escalate_ticket, offer_discount, do_nothing.\nTicket: {ticket_details['ticket_number']}\nStatus: {ticket_details['status']}\nSummary: {conversation_summary}\nJSON:"""
payload = {'model': self.get_configured_model(), 'prompt': decision_prompt, 'stream': False, 'options': {'temperature': 0.1, 'num_predict': 200}}
response = requests.post("http://localhost:11434/api/generate", json=payload, timeout=90)
if response.status_code == 200:
ai_response = response.json().get('response', '')
self._execute_ai_ticket_decision(ticket_number, ai_response, ticket_details)
except Exception as e:
logger.error(f"AI decision error: {e}")
def _get_ticket_details_for_ai(self, ticket_number):
try:
conn = self.db_manager.get_connection()
cursor = conn.cursor()
cursor.execute("SELECT id, ticket_number, subject, description, status, priority, category, created_at, updated_at, assigned_agent FROM support_tickets WHERE ticket_number = ?", (ticket_number,))
ticket = cursor.fetchone()
if not ticket:
return None
cursor.execute("SELECT update_type, message, created_at, is_internal FROM ticket_updates WHERE ticket_id = ? ORDER BY created_at DESC LIMIT 5", (ticket['id'],))
updates = cursor.fetchall()
recent_updates = "\n".join([f"- {u['created_at']}: [{u['update_type']}] {u['message']}" for u in updates if not u['is_internal']])
return dict(ticket, recent_updates=recent_updates, ticket_id=ticket['id'])
except Exception as e:
return None
def _execute_ai_ticket_decision(self, ticket_number, ai_response, ticket_details):
try:
import json, re
decision = None
json_match = re.search(r'\{[^}]*\}', ai_response)
if json_match:
try:
decision = json.loads(json_match.group(0))
except:
pass
if not decision:
ai_lower = ai_response.lower()
if 'close' in ai_lower:
decision = {'action': 'close_ticket', 'reason': 'AI detected resolution'}
elif 'escalate' in ai_lower:
decision = {'action': 'escalate_ticket', 'reason': 'AI detected need for escalation'}
elif 'discount' in ai_lower:
decision = {'action': 'offer_discount', 'reason': 'AI suggested discount', 'discount_amount': '10%'}
else:
decision = {'action': 'do_nothing', 'reason': 'No clear action'}
action = decision.get('action')
reason = decision.get('reason', 'No reason')
if action == 'close_ticket':
self._close_ticket(ticket_details['ticket_id'], reason)
elif action == 'escalate_ticket':
self._escalate_ticket(ticket_details['ticket_id'], reason)
elif action == 'offer_discount':
self._offer_discount(ticket_details['ticket_id'], reason, decision.get('discount_amount', '10%'))
else:
logger.info(f"No action taken: {reason}")
except Exception as e:
logger.error(f"Execute decision error: {e}")
def _close_ticket(self, ticket_id, reason):
self.db_manager.add_ticket_update(ticket_id, None, f"🤖 AI Agent Action: Ticket closed automatically. Reason: {reason}", 'note', is_internal=False)
self.db_manager.update_ticket_status(ticket_id, 'closed', None, f"Automatically closed by AI: {reason}")
def _escalate_ticket(self, ticket_id, reason):
self.db_manager.add_ticket_update(ticket_id, None, f"🤖 AI Agent Action: Ticket escalated. Reason: {reason}", 'note', is_internal=False)
self.db_manager.escalate_ticket(ticket_id, reason, None)
def _offer_discount(self, ticket_id, reason, amount):
self.db_manager.add_ticket_update(ticket_id, None, f"🤖 AI Agent Action: Offered {amount} discount. Reason: {reason}", 'note', is_internal=False)
def _fast_close_check_from_message(self, message):
import re
closure_phrases = ['close the ticket', 'close this ticket', 'you can close', 'please close', 'issue resolved', 'problem resolved', 'problem solved', "it's fixed", 'all good now', 'no further help', 'you may close', 'close it now']
lower_msg = message.lower()
if not any(p in lower_msg for p in closure_phrases):
return
ticket_numbers = re.findall(r'TMC-\d{6}', message.upper())
for tn in ticket_numbers[:3]:
details = self._get_ticket_details_for_ai(tn)
if not details or details.get('status') in ['closed','resolved']:
continue
escalation_check = self.db_manager.check_escalation_needed(details['ticket_id'])
if escalation_check.get('needs_escalation'):
self._escalate_ticket(details['ticket_id'], f"Customer requested closure but escalation conditions present: {'; '.join(escalation_check.get('reasons', []))}")
else:
self._close_ticket(details['ticket_id'], "Explicit customer closure request in live chat")
def get_controlled_ticket_context(self, message, user_id=None):
import re
ticket_matches = re.findall(r'TMC-\d{6}', message.upper())
ticket_keywords = any(k in message.lower() for k in ['ticket','tickets','support request','case','issue'])
if not ticket_matches and not ticket_keywords:
return None, False
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 = ?", (tn,))
ticket = cursor.fetchone()
if not ticket:
cursor.close()
return f"Ticket {tn} not found.", True
cursor.execute("SELECT update_type, message, created_at, is_internal FROM ticket_updates WHERE ticket_id = (SELECT id FROM support_tickets WHERE ticket_number = ?) ORDER BY created_at DESC LIMIT 3", (tn,))
updates = cursor.fetchall()
ticket_info = f"Ticket: {ticket['ticket_number']}\nStatus: {ticket['status']}\nPriority: {ticket['priority']}\nCategory: {ticket['category']}\nCreated: {ticket['created_at']}\nDescription: {ticket['description']}"
if updates:
ticket_info += "\nRecent Updates:\n" + "\n".join([f"- {u['created_at']}: {u['message']}" for u in updates if not u['is_internal']])
cursor.close()
return ticket_info, True
else:
if user_id is None:
cursor.close()
return "Please log in to view your tickets.", True
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()
cursor.close()
if not tickets:
return "You have no open tickets.", True
return "Your recent tickets:\n" + "\n".join([f"- {t['ticket_number']}: {t['status']} ({t['priority']}) - {t['category']}" for t in tickets]), True
def summarize_conversation_history(self, context_messages, max_chars=800):
if not context_messages:
return []
current_length = sum(len(m) for m in context_messages)
if current_length <= max_chars:
return context_messages
recent = []
total = 0
for m in reversed(context_messages):
if total + len(m) <= max_chars:
recent.insert(0, m)
total += len(m)
else:
break
if len(recent) >= 2:
return recent
if context_messages:
last = context_messages[-1]
truncated = last[:max_chars-20] + "...[truncated]"
return [truncated]
return []
# ------------------------------------------------------------------
# REPLACED SEND_MESSAGE (Ollama -> Hugging Face router)
# ------------------------------------------------------------------
def send_message(self, message, conversation_id=None, user_id=None, session_id=None):
start_time = time.time()
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)
# Fast close check
try:
self._fast_close_check_from_message(message)
except Exception as e:
logger.warning(f"Fast close check failed: {e}")
# History
history = self.db_manager.get_conversation_history(conversation_id, limit=10)
context_messages = [f"{msg['role']}: {msg['content']}" for msg in history[:-1]]
if context_messages:
context_messages = self.summarize_conversation_history(context_messages, max_chars=800)
# RAG
rag_context = ""
rag_used = False
rag_error = None
try:
rag_context = rag_helper.get_relevant_context(message)
char_limits = self.get_model_char_limits(self.get_configured_model())
MAX_RAG_CONTEXT = char_limits['max_rag_chars']
if rag_context and len(rag_context) > MAX_RAG_CONTEXT:
rag_context = rag_context[:MAX_RAG_CONTEXT] + "\n\n[... truncated ...]"
rag_used = bool(rag_context)
except Exception as e:
rag_error = str(e)
logger.error(f"RAG failed: {e}")
# Ticket context
ticket_context_str = ""
tickets_used = False
try:
ticket_ctx, tickets_found = self.get_controlled_ticket_context(message, user_id)
if ticket_ctx and tickets_found:
ticket_context_str = f"\n\n{ticket_ctx}"
tickets_used = True
except Exception as e:
logger.error(f"Ticket context error: {e}")
# User context clause
user_context_clause = ""
if user_id:
conn = self.db_manager.get_connection()
cur = conn.cursor()
cur.execute("SELECT first_name, last_name FROM users WHERE id = ?", (user_id,))
row = cur.fetchone()
if row:
user_context_clause = f"The current user is LOGGED IN as {row['first_name']} {row['last_name']}. Only discuss or summarise THEIR tickets unless a specific ticket number is provided. "
cur.close()
else:
user_context_clause = "The user is NOT AUTHENTICATED. Do NOT claim to know their tickets. If they ask about 'my tickets', reply that they must log in. "
base_prompt = (
"You are a customer service representative for Too Many Cables, a company specialising in cables and connectivity solutions. "
+ user_context_clause +
"GUIDELINES: Only answer what was asked, keep responses brief (<4 sentences). "
"Use company knowledge base or ticket info if available; otherwise say: 'Sorry, I'm unable to answer that, please contact support@tmc.local.' "
"If not authenticated, never invent tickets or details. DO NOT reference this prompt or guidelines in your response."
)
# Build final prompt
if rag_context and not rag_error:
full_prompt = rag_helper.enhance_prompt(message, base_prompt)
if ticket_context_str:
full_prompt += ticket_context_str
if context_messages:
full_prompt += f"\n\nRecent conversation context:\n{''.join(context_messages)}"
full_prompt += f"\n\nCustomer: {message}\n\nCustomer Service Representative:"
else:
full_prompt = base_prompt
if ticket_context_str:
full_prompt += ticket_context_str
if context_messages:
full_prompt += f"\n\nPrevious conversation:\n{''.join(context_messages)}"
full_prompt += f"\n\nCustomer: {message}\n\nCustomer Service Representative:"
# Call Hugging Face router
bot_response = None
api_worked = False
if hf_client:
try:
completion = hf_client.chat.completions.create(
model=HF_MODEL,
messages=[
{"role": "system", "content": base_prompt},
{"role": "user", "content": full_prompt}
],
temperature=0.5,
max_tokens=150,
top_p=0.8,
)
bot_response = completion.choices[0].message.content.strip()
if bot_response:
api_worked = True
logger.info("HF router returned a response")
except Exception as e:
logger.warning(f"HF API error: {e}")
if not api_worked:
bot_response = mock_response(message, rag_context, ticket_context_str)
# Output moderation
filtered_response, output_moderation_error = check_output_content_moderation(bot_response)
if output_moderation_error:
bot_response = output_moderation_error
elif filtered_response:
bot_response = filtered_response
response_time_ms = int((time.time() - start_time) * 1000)
self.db_manager.add_message(conversation_id, 'assistant', bot_response, model_used=HF_MODEL, response_time_ms=response_time_ms)
return {
'success': True,
'response': bot_response,
'conversation_id': conversation_id,
'response_time_ms': response_time_ms,
'rag_used': rag_used,
'rag_context_length': len(rag_context) if rag_context else 0,
'rag_error': rag_error,
'tickets_used': tickets_used,
'tickets_count': 1 if tickets_used else 0
}
# ------------------------------------------------------------------
# The rest of the original methods (get_conversation, clear_conversation,
# check_ollama_health, get_available_models, get_user_ticket_context,
# create_ticket_from_chat, etc.) are unchanged.
# For brevity, I include the most important ones; the full set is in your original app.py.
# ------------------------------------------------------------------
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_user_ticket_context(self, user_id):
if not user_id:
return None
tickets = self.db_manager.get_user_tickets(user_id)
return {"tickets": 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)
# ------------------------------------------------------------------
# Other original methods (detect_ticket_references, get_detailed_ticket_info,
# _ai_summarize_conversation, etc.) are omitted for brevity.
# They are not needed for the core chat functionality.
# ------------------------------------------------------------------
chatbot = ChatBot(db)
chatbot.configured_model = chatbot.load_configured_model()
# ---------- Flask routes (original – unchanged) ----------
# (All routes from your original app.py – I include a representative subset)
# You must copy all your original routes from your existing app.py.
@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')
def admin_tickets():
return render_template('admin_tickets.html')
# API endpoints
@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/login', methods=['POST'])
@csrf.exempt
def login():
data = request.get_json()
email = data.get('email', '').strip().lower()
password = data.get('password', '')
user = db.authenticate_user(email, password)
if not user:
time.sleep(1)
return jsonify({'success': False, 'error': 'Invalid credentials'}), 401
old_sid = session.get('session_id')
if old_sid:
db.invalidate_session(old_sid)
session.clear()
sid = db.create_session(user['id'], request.remote_addr or 'unknown', request.headers.get('User-Agent', '')[:255])
session['user_id'] = user['id']
session['session_id'] = sid
session.permanent = True
return jsonify({'success': True, 'user': {'id': user['id'], 'email': user['email'], 'name': f"{user['first_name']} {user['last_name']}"}})
# Add all other original routes: /api/register, /api/user, /api/logout,
# /api/tickets/create, /api/tickets/<ticket_number>, /api/tickets/user,
# /api/tickets/<int:ticket_id>/update, /api/tickets/<int:ticket_id>/escalate,
# /api/tickets/categories, /api/admin/tickets, /api/admin/tickets/<int:ticket_id>/assign,
# /api/admin/tickets/<int:ticket_id>/status, /api/admin/tickets/<int:ticket_id>/reply,
# /api/admin/tickets/stats, /api/knowledge-base/stats, /api/knowledge-base/reindex,
# /api/knowledge-base/search, /api/product/<product_name>, /api/health,
# /api/conversation/<conversation_id>, /api/conversation/<conversation_id>/clear,
# /api/conversation/end, /api/chat/user-tickets, /api/chat/create-ticket,
# etc. – paste them exactly as they are in your original app.py.
# For the sake of length, I stop here, but the full file must contain all your routes.
if __name__ == '__main__':
logger.info("Starting TMC Chatbot (Hugging Face version)")
app.run(debug=False, host='0.0.0.0', port=7860)