Spaces:
Runtime error
Runtime error
| 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. | |
| def homepage(): | |
| return render_template('homepage.html') | |
| def products(): | |
| return render_template('products.html') | |
| def chat(): | |
| cache_bust = int(time.time()) | |
| return render_template('chat.html', configured_model=chatbot.get_configured_model(), cache_bust=cache_bust) | |
| def tickets(): | |
| return render_template('tickets.html') | |
| def admin(): | |
| return redirect(url_for('admin_tickets')) | |
| def admin_tickets(): | |
| return render_template('admin_tickets.html') | |
| # API endpoints | |
| 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) | |
| 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) | |