Spaces:
Sleeping
Sleeping
| import os | |
| import random | |
| import time | |
| import json | |
| from datetime import datetime, timedelta | |
| from flask import Flask, render_template, jsonify, request | |
| from faker import Faker | |
| app = Flask(__name__) | |
| fake = Faker('zh_CN') | |
| # --- Configuration --- | |
| PORT = 7860 | |
| UPLOAD_FOLDER = 'uploads' | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| # --- In-Memory Data Store --- | |
| tickets = [] | |
| agents = [ | |
| {"id": 1, "name": "王强", "role": "L1 Support", "status": "online", "load": 0, "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=王强"}, | |
| {"id": 2, "name": "李娜", "role": "L2 Support", "status": "busy", "load": 4, "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=李娜"}, | |
| {"id": 3, "name": "张伟", "role": "Tech Lead", "status": "online", "load": 1, "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=张伟"}, | |
| {"id": 4, "name": "刘洋", "role": "Billing Specialist", "status": "offline", "load": 0, "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=刘洋"}, | |
| {"id": 5, "name": "陈敏", "role": "L1 Support", "status": "online", "load": 2, "avatar": "https://api.dicebear.com/7.x/avataaars/svg?seed=陈敏"}, | |
| ] | |
| # --- Business Logic & Simulation --- | |
| KEYWORDS_URGENT = ["投诉", "退款", "崩溃", "无法登录", "数据丢失", "紧急", "报错", "严重"] | |
| KEYWORDS_BILLING = ["发票", "扣款", "续费", "价格", "支付", "账单"] | |
| KEYWORDS_TECH = ["API", "Bug", "连接", "延迟", "配置", "代码", "服务器", "数据库"] | |
| def calculate_sentiment(text): | |
| """ | |
| Simulate sentiment analysis. | |
| Returns score -1.0 (Negative) to 1.0 (Positive). | |
| """ | |
| negative_words = ["失望", "垃圾", "慢", "差", "无法", "错误", "失败", "投诉", "骗子", "恶心", "烦", "糟糕"] | |
| positive_words = ["谢谢", "棒", "好", "解决", "顺利", "喜欢", "感谢", "赞", "优秀", "完美"] | |
| score = 0.0 | |
| for w in negative_words: | |
| if w in text: | |
| score -= 0.3 | |
| for w in positive_words: | |
| if w in text: | |
| score += 0.3 | |
| # Random noise | |
| score += random.uniform(-0.1, 0.1) | |
| return max(min(score, 1.0), -1.0) | |
| def auto_triage(ticket): | |
| """ | |
| AI Triage Logic: Assign Priority and Category based on content. | |
| """ | |
| text = ticket['content'] + " " + ticket['subject'] | |
| # Category Detection | |
| if any(k in text for k in KEYWORDS_BILLING): | |
| ticket['category'] = 'Billing' | |
| elif any(k in text for k in KEYWORDS_TECH): | |
| ticket['category'] = 'Technical' | |
| else: | |
| ticket['category'] = 'General' | |
| # Priority Detection | |
| if any(k in text for k in KEYWORDS_URGENT): | |
| ticket['priority'] = 'High' | |
| ticket['tags'].append('Risk') | |
| elif ticket['category'] == 'Billing': | |
| ticket['priority'] = 'Medium' | |
| else: | |
| ticket['priority'] = 'Low' | |
| # Sentiment | |
| ticket['sentiment_score'] = calculate_sentiment(text) | |
| return ticket | |
| def generate_ticket(): | |
| """Generate a realistic support ticket.""" | |
| created_at = fake.date_time_between(start_date='-2d', end_date='now') | |
| # Generate somewhat context-aware content | |
| prob = random.random() | |
| if prob < 0.2: | |
| subject = f"紧急:{fake.sentence(nb_words=3)}" | |
| content = f"我的系统彻底崩溃了,无法登录!报错代码500。{fake.text()}" | |
| elif prob < 0.4: | |
| subject = f"关于发票的问题" | |
| content = f"请问上个月的扣款为什么多出了50元?需要解释。{fake.text()}" | |
| elif prob < 0.7: | |
| subject = f"功能咨询:{fake.word()}" | |
| content = f"我想知道如何配置API密钥,文档看不懂。{fake.text()}" | |
| else: | |
| subject = fake.sentence(nb_words=4) | |
| content = fake.text() | |
| ticket = { | |
| "id": fake.uuid4()[:8].upper(), | |
| "subject": subject, | |
| "content": content, | |
| "customer_email": fake.email(), | |
| "customer_name": fake.name(), | |
| "status": "New", | |
| "created_at": created_at.strftime("%Y-%m-%d %H:%M:%S"), | |
| "tags": [], | |
| "history": [], | |
| "attachments": [] | |
| } | |
| return auto_triage(ticket) | |
| # Initial Population | |
| for _ in range(20): | |
| tickets.append(generate_ticket()) | |
| # Sort by date | |
| tickets.sort(key=lambda x: x['created_at'], reverse=True) | |
| # --- Routes --- | |
| def index(): | |
| return render_template('index.html') | |
| def get_stats(): | |
| total = len(tickets) | |
| open_tickets = len([t for t in tickets if t['status'] != 'Resolved']) | |
| high_priority = len([t for t in tickets if t['priority'] == 'High' and t['status'] != 'Resolved']) | |
| # Calculate avg sentiment of last 50 tickets | |
| recent = tickets[:50] | |
| avg_sentiment = sum(t['sentiment_score'] for t in recent) / len(recent) if recent else 0 | |
| # Agent load | |
| agent_stats = agents | |
| return jsonify({ | |
| "total_tickets": total, | |
| "open_tickets": open_tickets, | |
| "high_priority_risk": high_priority, | |
| "customer_happiness": round((avg_sentiment + 1) * 50, 1), # Scale -1..1 to 0..100 | |
| "agents": agent_stats | |
| }) | |
| def get_tickets(): | |
| # Return recent 50 tickets | |
| return jsonify(tickets[:50]) | |
| def simulate_incoming(): | |
| """Simulate receiving 1-3 new tickets""" | |
| count = random.randint(1, 3) | |
| new_tickets = [] | |
| for _ in range(count): | |
| t = generate_ticket() | |
| # Set to very recent | |
| t['created_at'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| tickets.insert(0, t) | |
| new_tickets.append(t) | |
| # Auto-assign to least loaded online agent | |
| online_agents = [a for a in agents if a['status'] == 'online'] | |
| if online_agents: | |
| for t in new_tickets: | |
| online_agents.sort(key=lambda x: x['load']) | |
| target = online_agents[0] | |
| target['load'] += 1 | |
| t['assigned_to'] = target['name'] | |
| t['status'] = 'Open' | |
| return jsonify({"added": count, "tickets": new_tickets}) | |
| def upload_file(): | |
| if 'file' not in request.files: | |
| return jsonify({'error': 'No file part'}), 400 | |
| file = request.files['file'] | |
| if file.filename == '': | |
| return jsonify({'error': 'No selected file'}), 400 | |
| if file: | |
| filename = file.filename | |
| # In a real app, use secure_filename and unique IDs | |
| filepath = os.path.join(UPLOAD_FOLDER, filename) | |
| file.save(filepath) | |
| return jsonify({'success': True, 'filename': filename, 'url': f'/uploads/{filename}'}) | |
| def resolve_ticket(): | |
| """Simulate resolving tickets to keep list clean""" | |
| data = request.json | |
| ticket_id = data.get('id') | |
| for t in tickets: | |
| if t['id'] == ticket_id: | |
| t['status'] = 'Resolved' | |
| # Reduce load of assigned agent | |
| if 'assigned_to' in t: | |
| for a in agents: | |
| if a['name'] == t['assigned_to'] and a['load'] > 0: | |
| a['load'] -= 1 | |
| return jsonify({"success": True}) | |
| return jsonify({"success": False}), 404 | |
| def page_not_found(e): | |
| return render_template('index.html'), 404 # SPA fallback mostly | |
| def internal_server_error(e): | |
| return jsonify(error=str(e)), 500 | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=PORT, debug=True) | |