Spaces:
Runtime error
Remove all non-MCP modules (CX platform components)
Browse filesARCHITECTURAL SIMPLIFICATION:
Removed all modules that don't use MCP (Model Context Protocol).
The application now exclusively uses MCP for all operations.
REMOVED MODULES (Direct DB access, not MCP-compatible):
✗ modules/tickets/ - Ticket management system
✗ modules/chat/ - Live chat system
✗ modules/knowledge/ - Knowledge base system
✗ modules/analytics/ - Analytics dashboard
REMOVED FILES:
✗ app_enterprise.py - Separate enterprise UI (not using MCP)
✗ models/cx_models.py - CX platform database models (20+ tables)
✗ database/init_db.py - CX sample data initialization
✗ CX_PLATFORM_ARCHITECTURE.md - CX platform documentation
✗ CX_PLATFORM_SUMMARY.md - CX platform summary
UPDATED FILES:
✓ app.py - Removed CX module imports and UI tabs
✓ database/manager.py - Removed CX table creation and sample data
RESULT:
The codebase is now 100% MCP-based:
- ✅ B2B Sales Pipeline → Uses MCP (Store, Search, Email, Calendar)
- ✅ Discovery Services → Use MCP Search Client
- ✅ Research Services → Use MCP Search Client
- ✅ Contact Finder → Uses MCP Search Client
- ✅ B2BSalesAgent → Fully MCP-integrated
All remaining functionality leverages MCP as requested!
Files removed: 20+ files
Lines removed: ~2500+ lines of non-MCP code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
- .claude/settings.local.json +2 -1
- CX_PLATFORM_ARCHITECTURE.md +0 -649
- CX_PLATFORM_SUMMARY.md +0 -439
- app.py +3 -25
- app_enterprise.py +0 -533
- database/init_db.py +0 -230
- database/manager.py +1 -104
- models/cx_models.py +0 -598
- modules/analytics/__init__.py +0 -9
- modules/analytics/manager.py +0 -346
- modules/analytics/ui.py +0 -248
- modules/chat/__init__.py +0 -10
- modules/chat/bot.py +0 -360
- modules/chat/manager.py +0 -427
- modules/chat/ui.py +0 -331
- modules/knowledge/__init__.py +0 -10
- modules/knowledge/manager.py +0 -475
- modules/knowledge/search.py +0 -361
- modules/knowledge/ui.py +0 -445
- modules/tickets/__init__.py +0 -9
- modules/tickets/manager.py +0 -552
- modules/tickets/ui.py +0 -506
|
@@ -8,7 +8,8 @@
|
|
| 8 |
"Bash(python:*)",
|
| 9 |
"Bash(git add:*)",
|
| 10 |
"Bash(git restore:*)",
|
| 11 |
-
"Bash(git commit:*)"
|
|
|
|
| 12 |
],
|
| 13 |
"deny": [],
|
| 14 |
"ask": []
|
|
|
|
| 8 |
"Bash(python:*)",
|
| 9 |
"Bash(git add:*)",
|
| 10 |
"Bash(git restore:*)",
|
| 11 |
+
"Bash(git commit:*)",
|
| 12 |
+
"Bash(cat:*)"
|
| 13 |
],
|
| 14 |
"deny": [],
|
| 15 |
"ask": []
|
|
@@ -1,649 +0,0 @@
|
|
| 1 |
-
# CX AI Platform - Comprehensive Architecture
|
| 2 |
-
|
| 3 |
-
## Vision
|
| 4 |
-
|
| 5 |
-
Transform the pipeline demo into a **full-fledged Customer Experience (CX) platform** powered by AI agents and MCP servers, with all functionalities accessible through the main Gradio app (app.py).
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## 🏗️ Platform Architecture
|
| 10 |
-
|
| 11 |
-
```
|
| 12 |
-
┌─────────────────────────────────────────────────────────────────┐
|
| 13 |
-
│ GRADIO APP (app.py) │
|
| 14 |
-
│ Main Interface & Orchestrator │
|
| 15 |
-
├─────────────────────────────────────────────────────────────────┤
|
| 16 |
-
│ Dashboard │ Tickets │ Customers │ KB │ Analytics │ Settings │
|
| 17 |
-
└──────┬──────────────────────────────────────────────────────────┘
|
| 18 |
-
│
|
| 19 |
-
├─────────────── AI AGENT LAYER ─────────────────────
|
| 20 |
-
│
|
| 21 |
-
├─► Support Agent (Ticket handling, routing, responses)
|
| 22 |
-
├─► Knowledge Agent (KB search, content generation)
|
| 23 |
-
├─► Sentiment Agent (Emotion analysis, priority scoring)
|
| 24 |
-
├─► Resolution Agent (Solution finding, automation)
|
| 25 |
-
├─► Outreach Agent (Proactive engagement, campaigns)
|
| 26 |
-
├─► Analytics Agent (Insights, reporting, predictions)
|
| 27 |
-
│
|
| 28 |
-
├─────────────── MODULE LAYER ──────────────────────
|
| 29 |
-
│
|
| 30 |
-
├─► Ticket Management (Create, track, resolve, SLA)
|
| 31 |
-
├─► Customer Management (Profiles, history, segments)
|
| 32 |
-
├─► Knowledge Base (Articles, FAQs, search, RAG)
|
| 33 |
-
├─► Live Chat (Real-time support, chatbot)
|
| 34 |
-
├─► Email Integration (Inbox, replies, automation)
|
| 35 |
-
├─► Analytics & Insights (Metrics, reports, trends)
|
| 36 |
-
├─► Automation & Workflows (Rules, triggers, sequences)
|
| 37 |
-
├─► Team Collaboration (Assignments, notes, handoffs)
|
| 38 |
-
│
|
| 39 |
-
├─────────────── MCP SERVER LAYER ──────────────────
|
| 40 |
-
│
|
| 41 |
-
├─► Search MCP (Serper API - web search)
|
| 42 |
-
├─► Email MCP (SMTP/IMAP - email operations)
|
| 43 |
-
├─► Calendar MCP (Meeting scheduling)
|
| 44 |
-
├─► Store MCP (Data persistence)
|
| 45 |
-
├─► Chat MCP (Real-time messaging)
|
| 46 |
-
├─► CRM MCP (Customer data sync)
|
| 47 |
-
│
|
| 48 |
-
└─────────────── DATA LAYER ────────────────────────
|
| 49 |
-
│
|
| 50 |
-
├─► SQLite Database (tickets, customers, kb, etc.)
|
| 51 |
-
├─► Vector Store (FAISS - semantic search)
|
| 52 |
-
├─► Cache (Redis/in-memory - real-time data)
|
| 53 |
-
└─► File Storage (attachments, exports)
|
| 54 |
-
```
|
| 55 |
-
|
| 56 |
-
---
|
| 57 |
-
|
| 58 |
-
## 📦 Module Structure
|
| 59 |
-
|
| 60 |
-
```
|
| 61 |
-
cx_ai_agent/
|
| 62 |
-
├── app.py # Main Gradio application (entry point)
|
| 63 |
-
│
|
| 64 |
-
├── modules/ # Feature modules
|
| 65 |
-
│ ├── __init__.py
|
| 66 |
-
│ ├── tickets/ # Ticket management
|
| 67 |
-
│ │ ├── __init__.py
|
| 68 |
-
│ │ ├── manager.py # Ticket CRUD operations
|
| 69 |
-
│ │ ├── router.py # Smart routing logic
|
| 70 |
-
│ │ ├── sla.py # SLA tracking
|
| 71 |
-
│ │ └── ui.py # Gradio UI components
|
| 72 |
-
│ │
|
| 73 |
-
│ ├── customers/ # Customer management
|
| 74 |
-
│ │ ├── __init__.py
|
| 75 |
-
│ │ ├── manager.py # Customer profiles
|
| 76 |
-
│ │ ├── segments.py # Customer segmentation
|
| 77 |
-
│ │ ├── journey.py # Journey mapping
|
| 78 |
-
│ │ └── ui.py
|
| 79 |
-
│ │
|
| 80 |
-
│ ├── knowledge/ # Knowledge base
|
| 81 |
-
│ │ ├── __init__.py
|
| 82 |
-
│ │ ├── manager.py # Article management
|
| 83 |
-
│ │ ├── search.py # Semantic search
|
| 84 |
-
│ │ ├── generator.py # AI content generation
|
| 85 |
-
│ │ └── ui.py
|
| 86 |
-
│ │
|
| 87 |
-
│ ├── chat/ # Live chat
|
| 88 |
-
│ │ ├── __init__.py
|
| 89 |
-
│ │ ├── manager.py # Chat sessions
|
| 90 |
-
│ │ ├── bot.py # AI chatbot
|
| 91 |
-
│ │ ├── handoff.py # Human handoff
|
| 92 |
-
│ │ └── ui.py
|
| 93 |
-
│ │
|
| 94 |
-
│ ├── email/ # Email integration
|
| 95 |
-
│ │ ├── __init__.py
|
| 96 |
-
│ │ ├── inbox.py # Email fetching
|
| 97 |
-
│ │ ├���─ sender.py # Email sending
|
| 98 |
-
│ │ ├── templates.py # Email templates
|
| 99 |
-
│ │ └── ui.py
|
| 100 |
-
│ │
|
| 101 |
-
│ ├── analytics/ # Analytics & insights
|
| 102 |
-
│ │ ├── __init__.py
|
| 103 |
-
│ │ ├── metrics.py # KPI calculations
|
| 104 |
-
│ │ ├── reports.py # Report generation
|
| 105 |
-
│ │ ├── forecasting.py # Predictive analytics
|
| 106 |
-
│ │ └── ui.py
|
| 107 |
-
│ │
|
| 108 |
-
│ ├── automation/ # Workflows & automation
|
| 109 |
-
│ │ ├── __init__.py
|
| 110 |
-
│ │ ├── rules.py # Business rules
|
| 111 |
-
│ │ ├── triggers.py # Event triggers
|
| 112 |
-
│ │ ├── workflows.py # Workflow engine
|
| 113 |
-
│ │ └── ui.py
|
| 114 |
-
│ │
|
| 115 |
-
│ └── settings/ # Platform settings
|
| 116 |
-
│ ├── __init__.py
|
| 117 |
-
│ ├── config.py # Configuration
|
| 118 |
-
│ ├── users.py # User management
|
| 119 |
-
│ └── ui.py
|
| 120 |
-
│
|
| 121 |
-
├── agents/ # AI agents (existing + new)
|
| 122 |
-
│ ├── support_agent.py # NEW: Ticket handling
|
| 123 |
-
│ ├── knowledge_agent.py # NEW: KB operations
|
| 124 |
-
│ ├── sentiment_agent.py # NEW: Emotion analysis
|
| 125 |
-
│ ├── resolution_agent.py # NEW: Problem solving
|
| 126 |
-
│ ├── hunter.py # Existing: Prospect discovery
|
| 127 |
-
│ ├── enricher.py # Existing: Data enrichment
|
| 128 |
-
│ ├── writer.py # Existing: Content generation
|
| 129 |
-
│ └── ... (other existing agents)
|
| 130 |
-
│
|
| 131 |
-
├── mcp/ # MCP servers
|
| 132 |
-
│ ├── servers/
|
| 133 |
-
│ │ ├── search_server.py # Existing: Web search
|
| 134 |
-
│ │ ├── email_server.py # Existing: Email
|
| 135 |
-
│ │ ├── chat_server.py # NEW: Live chat
|
| 136 |
-
│ │ ├── crm_server.py # NEW: CRM integration
|
| 137 |
-
│ │ └── notification_server.py # NEW: Notifications
|
| 138 |
-
│ └── ... (other MCP components)
|
| 139 |
-
│
|
| 140 |
-
├── database/ # Data layer
|
| 141 |
-
│ ├── schema.sql # Existing + extended
|
| 142 |
-
│ ├── manager.py # Existing
|
| 143 |
-
│ └── migrations/ # Database migrations
|
| 144 |
-
│
|
| 145 |
-
├── models/ # Data models
|
| 146 |
-
│ ├── database.py # Existing + extended
|
| 147 |
-
│ ├── ticket.py # NEW: Ticket models
|
| 148 |
-
│ ├── customer.py # NEW: Customer models
|
| 149 |
-
│ └── knowledge.py # NEW: KB models
|
| 150 |
-
│
|
| 151 |
-
├── ui/ # UI components
|
| 152 |
-
│ ├── theme.py # Existing
|
| 153 |
-
│ ├── components.py # NEW: Reusable components
|
| 154 |
-
│ └── layouts.py # NEW: Page layouts
|
| 155 |
-
│
|
| 156 |
-
└── api/ # API layer
|
| 157 |
-
├── __init__.py
|
| 158 |
-
├── routes.py # FastAPI routes
|
| 159 |
-
└── webhooks.py # Webhook handlers
|
| 160 |
-
```
|
| 161 |
-
|
| 162 |
-
---
|
| 163 |
-
|
| 164 |
-
## 🤖 AI Agents (Expanded)
|
| 165 |
-
|
| 166 |
-
### 1. Support Agent
|
| 167 |
-
**Purpose:** Handle customer support tickets
|
| 168 |
-
**Capabilities:**
|
| 169 |
-
- Auto-categorize tickets
|
| 170 |
-
- Route to appropriate team
|
| 171 |
-
- Suggest responses
|
| 172 |
-
- Escalate urgent issues
|
| 173 |
-
- Track SLA compliance
|
| 174 |
-
|
| 175 |
-
**MCP Tools:**
|
| 176 |
-
- Store MCP (ticket data)
|
| 177 |
-
- Search MCP (find solutions)
|
| 178 |
-
- Email MCP (send updates)
|
| 179 |
-
|
| 180 |
-
### 2. Knowledge Agent
|
| 181 |
-
**Purpose:** Manage knowledge base
|
| 182 |
-
**Capabilities:**
|
| 183 |
-
- Search KB articles (semantic)
|
| 184 |
-
- Generate new articles
|
| 185 |
-
- Update existing content
|
| 186 |
-
- Suggest related articles
|
| 187 |
-
- Answer questions from KB
|
| 188 |
-
|
| 189 |
-
**MCP Tools:**
|
| 190 |
-
- Store MCP (KB data)
|
| 191 |
-
- Search MCP (research)
|
| 192 |
-
- Vector Store (RAG)
|
| 193 |
-
|
| 194 |
-
### 3. Sentiment Agent
|
| 195 |
-
**Purpose:** Analyze customer emotions
|
| 196 |
-
**Capabilities:**
|
| 197 |
-
- Detect sentiment (positive/negative/neutral)
|
| 198 |
-
- Identify urgency
|
| 199 |
-
- Score priority
|
| 200 |
-
- Flag escalations
|
| 201 |
-
- Track satisfaction trends
|
| 202 |
-
|
| 203 |
-
**MCP Tools:**
|
| 204 |
-
- Store MCP (conversation history)
|
| 205 |
-
- Analytics (trending emotions)
|
| 206 |
-
|
| 207 |
-
### 4. Resolution Agent
|
| 208 |
-
**Purpose:** Find and apply solutions
|
| 209 |
-
**Capabilities:**
|
| 210 |
-
- Diagnose issues
|
| 211 |
-
- Find KB solutions
|
| 212 |
-
- Generate step-by-step guides
|
| 213 |
-
- Auto-resolve common issues
|
| 214 |
-
- Learn from resolutions
|
| 215 |
-
|
| 216 |
-
**MCP Tools:**
|
| 217 |
-
- Search MCP (find solutions)
|
| 218 |
-
- Store MCP (save resolutions)
|
| 219 |
-
- Vector Store (similar issues)
|
| 220 |
-
|
| 221 |
-
### 5. Outreach Agent (Existing - Enhanced)
|
| 222 |
-
**Purpose:** Proactive customer engagement
|
| 223 |
-
**Capabilities:**
|
| 224 |
-
- Discover opportunities
|
| 225 |
-
- Run campaigns
|
| 226 |
-
- Personalize messages
|
| 227 |
-
- Track engagement
|
| 228 |
-
- Measure ROI
|
| 229 |
-
|
| 230 |
-
### 6. Analytics Agent
|
| 231 |
-
**Purpose:** Generate insights
|
| 232 |
-
**Capabilities:**
|
| 233 |
-
- Calculate KPIs
|
| 234 |
-
- Identify trends
|
| 235 |
-
- Predict issues
|
| 236 |
-
- Recommend actions
|
| 237 |
-
- Generate reports
|
| 238 |
-
|
| 239 |
-
---
|
| 240 |
-
|
| 241 |
-
## 📊 Core Modules
|
| 242 |
-
|
| 243 |
-
### 1. Ticket Management Module
|
| 244 |
-
|
| 245 |
-
**Features:**
|
| 246 |
-
- Create tickets (manual, email, chat, API)
|
| 247 |
-
- Smart categorization (AI-powered)
|
| 248 |
-
- Priority scoring
|
| 249 |
-
- SLA tracking & alerts
|
| 250 |
-
- Assignment & routing
|
| 251 |
-
- Status workflow (New → Open → Pending → Resolved → Closed)
|
| 252 |
-
- Attachments support
|
| 253 |
-
- Internal notes
|
| 254 |
-
- Customer notifications
|
| 255 |
-
|
| 256 |
-
**UI Components:**
|
| 257 |
-
```
|
| 258 |
-
Ticket List View:
|
| 259 |
-
┌─────────────────────────────────────────────────────┐
|
| 260 |
-
│ [+ New Ticket] [Filter ▼] [Search...] │
|
| 261 |
-
├─────────────────────────────────────────────────────┤
|
| 262 |
-
│ ID Subject Customer Status Agent │
|
| 263 |
-
│ #123 Login issue John Doe 🔴 Open Sarah │
|
| 264 |
-
│ #122 Billing question Jane Smith ⏳ Pending Alice │
|
| 265 |
-
│ #121 Feature request Mike Chen ✅ Resolved Bob │
|
| 266 |
-
└─────────────────────────────────────────────────────┘
|
| 267 |
-
|
| 268 |
-
Ticket Detail View:
|
| 269 |
-
┌─────────────────────────────────────────────────────┐
|
| 270 |
-
│ #123 - Login issue 🔴 Open │
|
| 271 |
-
├─────────────────────────────────────────────────────┤
|
| 272 |
-
│ Customer: John Doe ([email protected]) │
|
| 273 |
-
│ Priority: High | SLA: 2h remaining │
|
| 274 |
-
│ Agent: Sarah | Category: Technical │
|
| 275 |
-
├─────────────────────────────────────────────────────┤
|
| 276 |
-
│ Conversation: │
|
| 277 |
-
│ [Customer message thread...] │
|
| 278 |
-
│ [Agent replies...] │
|
| 279 |
-
│ [AI suggested responses...] │
|
| 280 |
-
├─────────────────────────────────────────────────────┤
|
| 281 |
-
│ [💬 Reply] [📎 Attach] [✅ Resolve] [↗ Escalate]│
|
| 282 |
-
└─────────────────────────────────────────────────────┘
|
| 283 |
-
```
|
| 284 |
-
|
| 285 |
-
**Database Tables:**
|
| 286 |
-
```sql
|
| 287 |
-
tickets (id, customer_id, subject, description, status, priority,
|
| 288 |
-
category, assigned_to, sla_due_at, created_at, resolved_at)
|
| 289 |
-
|
| 290 |
-
ticket_messages (id, ticket_id, sender_type, sender_id, message,
|
| 291 |
-
is_internal, created_at)
|
| 292 |
-
|
| 293 |
-
ticket_attachments (id, ticket_id, filename, file_path, file_size)
|
| 294 |
-
```
|
| 295 |
-
|
| 296 |
-
### 2. Customer Management Module
|
| 297 |
-
|
| 298 |
-
**Features:**
|
| 299 |
-
- Customer profiles (360° view)
|
| 300 |
-
- Contact history (all interactions)
|
| 301 |
-
- Sentiment tracking
|
| 302 |
-
- Segmentation (VIP, at-risk, etc.)
|
| 303 |
-
- Custom fields
|
| 304 |
-
- Tags and labels
|
| 305 |
-
- Journey mapping
|
| 306 |
-
- Satisfaction scores (CSAT, NPS)
|
| 307 |
-
|
| 308 |
-
**UI Components:**
|
| 309 |
-
```
|
| 310 |
-
Customer Profile:
|
| 311 |
-
┌─────────────────────────────────────────────────────┐
|
| 312 |
-
│ 👤 John Doe VIP Customer │
|
| 313 |
-
│ [email protected] | +1-555-0123 │
|
| 314 |
-
│ Customer since: Jan 2023 | LTV: $12,450 │
|
| 315 |
-
├─────────────────────────────────────────────────────┤
|
| 316 |
-
│ 📊 Metrics: │
|
| 317 |
-
│ • Tickets: 23 (2 open) │
|
| 318 |
-
│ • CSAT: 4.5/5 ⭐ │
|
| 319 |
-
│ • Sentiment: 😊 Positive │
|
| 320 |
-
│ • Last contact: 2 days ago │
|
| 321 |
-
├─────────────────────────────────────────────────────┤
|
| 322 |
-
│ 🕒 Recent Activity: │
|
| 323 |
-
│ • Yesterday: Opened ticket #123 │
|
| 324 |
-
│ • 3 days ago: Purchased Premium plan │
|
| 325 |
-
│ • 1 week ago: Rated support 5/5 │
|
| 326 |
-
├─────────────────────────────────────────────────────┤
|
| 327 |
-
│ [📧 Email] [💬 Chat] [🎫 New Ticket] [📝 Note] │
|
| 328 |
-
└─────────────────────────────────────────────────────┘
|
| 329 |
-
```
|
| 330 |
-
|
| 331 |
-
**Database Tables:**
|
| 332 |
-
```sql
|
| 333 |
-
customers (id, name, email, phone, company, segment,
|
| 334 |
-
lifetime_value, satisfaction_score, created_at)
|
| 335 |
-
|
| 336 |
-
customer_attributes (customer_id, key, value)
|
| 337 |
-
|
| 338 |
-
customer_segments (id, name, criteria, color)
|
| 339 |
-
|
| 340 |
-
interaction_history (id, customer_id, type, channel,
|
| 341 |
-
summary, sentiment, created_at)
|
| 342 |
-
```
|
| 343 |
-
|
| 344 |
-
### 3. Knowledge Base Module
|
| 345 |
-
|
| 346 |
-
**Features:**
|
| 347 |
-
- Article management (CRUD)
|
| 348 |
-
- Categories & tags
|
| 349 |
-
- Semantic search (RAG)
|
| 350 |
-
- AI content generation
|
| 351 |
-
- Version control
|
| 352 |
-
- View analytics
|
| 353 |
-
- Related articles
|
| 354 |
-
- Multi-language support
|
| 355 |
-
- Public/internal articles
|
| 356 |
-
|
| 357 |
-
**UI Components:**
|
| 358 |
-
```
|
| 359 |
-
Knowledge Base:
|
| 360 |
-
┌─────────────────────────────────────────────────────┐
|
| 361 |
-
│ [+ New Article] [Categories ▼] [🔍 Search KB...] │
|
| 362 |
-
├─────────────────────────────────────────────────────┤
|
| 363 |
-
│ Categories: │
|
| 364 |
-
│ • Getting Started (12 articles) │
|
| 365 |
-
│ • Billing & Payments (8 articles) │
|
| 366 |
-
│ • Technical Issues (15 articles) │
|
| 367 |
-
│ • Account Management (10 articles) │
|
| 368 |
-
├─────────────────────────────────────────────────────┤
|
| 369 |
-
│ Popular Articles: │
|
| 370 |
-
│ 1. How to reset your password 👁 1,234 views │
|
| 371 |
-
│ 2. Setting up two-factor auth 👁 892 views │
|
| 372 |
-
│ 3. Understanding your bill 👁 756 views │
|
| 373 |
-
├─────────────────────────────────────────────────────┤
|
| 374 |
-
│ [AI-Generated Draft Articles: 3] │
|
| 375 |
-
└─────────────────────────────────────────────────────┘
|
| 376 |
-
|
| 377 |
-
Article Editor:
|
| 378 |
-
┌─────────────────────────────────────────────────────┐
|
| 379 |
-
│ Title: [How to reset your password_____________] │
|
| 380 |
-
│ Category: [Getting Started ▼] │
|
| 381 |
-
│ Tags: [password, security, account___________] │
|
| 382 |
-
├─────────────────────────────────────────────────────┤
|
| 383 |
-
│ Content: [Rich text editor...] │
|
| 384 |
-
│ [AI: Generate content] [AI: Improve clarity] │
|
| 385 |
-
├─────────────────────────────────────────────────────┤
|
| 386 |
-
│ Status: ● Published | Views: 1,234 | Rating: 4.5│
|
| 387 |
-
│ [💾 Save] [👁 Preview] [🗑 Delete] │
|
| 388 |
-
└─────────────────────────────────────────────────────┘
|
| 389 |
-
```
|
| 390 |
-
|
| 391 |
-
**Database Tables:**
|
| 392 |
-
```sql
|
| 393 |
-
kb_articles (id, title, content, category_id, status,
|
| 394 |
-
views, rating, created_at, updated_at)
|
| 395 |
-
|
| 396 |
-
kb_categories (id, name, parent_id, order)
|
| 397 |
-
|
| 398 |
-
kb_article_tags (article_id, tag)
|
| 399 |
-
|
| 400 |
-
kb_article_versions (id, article_id, content, version, created_at)
|
| 401 |
-
```
|
| 402 |
-
|
| 403 |
-
### 4. Live Chat Module
|
| 404 |
-
|
| 405 |
-
**Features:**
|
| 406 |
-
- Real-time chat widget
|
| 407 |
-
- AI chatbot (first response)
|
| 408 |
-
- Human handoff
|
| 409 |
-
- Canned responses
|
| 410 |
-
- Typing indicators
|
| 411 |
-
- File sharing
|
| 412 |
-
- Chat history
|
| 413 |
-
- Proactive chat triggers
|
| 414 |
-
- Chat routing
|
| 415 |
-
|
| 416 |
-
**UI Components:**
|
| 417 |
-
```
|
| 418 |
-
Live Chat Dashboard:
|
| 419 |
-
┌─────────────────────────────────────────────────────┐
|
| 420 |
-
│ Active Chats (3) | Waiting (1) | Bot Only (5) │
|
| 421 |
-
├─────────────────────────────────────────────────────┤
|
| 422 |
-
│ 💬 Jane Smith ⏱ 00:03:45 │
|
| 423 |
-
│ "Having trouble with payment..." │
|
| 424 |
-
│ [Take over from bot] │
|
| 425 |
-
├─────────────────────────────────────────────────────┤
|
| 426 |
-
│ 💬 Mike Chen ⏱ 00:01:20 │
|
| 427 |
-
│ "Need help with setup" │
|
| 428 |
-
│ [View conversation] │
|
| 429 |
-
└─────────────────────────────────────────────────────┘
|
| 430 |
-
|
| 431 |
-
Chat Window:
|
| 432 |
-
┌─────────────────────────────────────────────────────┐
|
| 433 |
-
│ 💬 Chat with Jane Smith │
|
| 434 |
-
├─────────────────────────────────────────────────────┤
|
| 435 |
-
│ [10:30] Jane: Having trouble with payment │
|
| 436 |
-
│ [10:31] Bot: I can help! What issue are you having?│
|
| 437 |
-
│ [10:32] Jane: Card declined │
|
| 438 |
-
│ [10:33] Bot: Let me get a human agent... │
|
| 439 |
-
│ [10:34] Sarah: Hi Jane! I'll help you with this. │
|
| 440 |
-
├─────────────────────────────────────────────────────┤
|
| 441 |
-
│ [Type message...] [📎] [😊] [▶]│
|
| 442 |
-
│ 💡 Suggested: "Let me check your payment method" │
|
| 443 |
-
└─────────────────────────────────────────────────────┘
|
| 444 |
-
```
|
| 445 |
-
|
| 446 |
-
### 5. Analytics & Insights Module
|
| 447 |
-
|
| 448 |
-
**Features:**
|
| 449 |
-
- Real-time dashboard
|
| 450 |
-
- Custom reports
|
| 451 |
-
- KPI tracking (CSAT, NPS, FCR, AHT)
|
| 452 |
-
- Trend analysis
|
| 453 |
-
- Agent performance
|
| 454 |
-
- Channel analytics
|
| 455 |
-
- Predictive insights
|
| 456 |
-
- Export (CSV, PDF)
|
| 457 |
-
|
| 458 |
-
**UI Components:**
|
| 459 |
-
```
|
| 460 |
-
Analytics Dashboard:
|
| 461 |
-
┌─────────────────────────────────────────────────────┐
|
| 462 |
-
│ Overview (Last 30 days) │
|
| 463 |
-
├─────────────────────────────────────────────────────┤
|
| 464 |
-
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐│
|
| 465 |
-
│ │ Tickets │ │ CSAT │ │ FCR │ │ AHT ││
|
| 466 |
-
│ │ 342 │ │ 4.5/5 │ │ 78% │ │ 12min ││
|
| 467 |
-
│ │ ↑ 12% │ │ ↑ 0.3 │ │ ↓ 5% │ │ ↓ 2min ││
|
| 468 |
-
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘│
|
| 469 |
-
├─────────────────────────────────────────────────────┤
|
| 470 |
-
│ Ticket Volume Trend: │
|
| 471 |
-
│ [Line chart showing daily ticket count...] │
|
| 472 |
-
├─────────────────────────────────────────────────────┤
|
| 473 |
-
│ Top Issues: │
|
| 474 |
-
│ 1. Login problems 42 tickets (12%) │
|
| 475 |
-
│ 2. Billing questions 38 tickets (11%) │
|
| 476 |
-
│ 3. Feature requests 32 tickets (9%) │
|
| 477 |
-
└─────────────────────────────────────────────────────┘
|
| 478 |
-
```
|
| 479 |
-
|
| 480 |
-
### 6. Automation & Workflows Module
|
| 481 |
-
|
| 482 |
-
**Features:**
|
| 483 |
-
- Rule builder (if-then)
|
| 484 |
-
- Triggers (time, event, condition)
|
| 485 |
-
- Actions (assign, tag, email, escalate)
|
| 486 |
-
- Templates & macros
|
| 487 |
-
- Auto-responses
|
| 488 |
-
- Scheduled tasks
|
| 489 |
-
- Workflow analytics
|
| 490 |
-
|
| 491 |
-
**UI Components:**
|
| 492 |
-
```
|
| 493 |
-
Automation Rules:
|
| 494 |
-
┌─────────────────────────────────────────────────────┐
|
| 495 |
-
│ [+ New Rule] │
|
| 496 |
-
├─────────────────────────────────────────────────────┤
|
| 497 |
-
│ Rule: Auto-escalate high priority │
|
| 498 |
-
│ ● Active │
|
| 499 |
-
│ │
|
| 500 |
-
│ IF: │
|
| 501 |
-
│ • Ticket priority = High │
|
| 502 |
-
│ • No response in 2 hours │
|
| 503 |
-
│ │
|
| 504 |
-
│ THEN: │
|
| 505 |
-
│ • Assign to: Manager │
|
| 506 |
-
│ • Send notification: Email + Slack │
|
| 507 |
-
│ • Add tag: "escalated" │
|
| 508 |
-
│ │
|
| 509 |
-
│ [✏️ Edit] [▶ Test] [📊 Stats] [🗑 Delete] │
|
| 510 |
-
└─────────────────────────────────────────────────────┘
|
| 511 |
-
```
|
| 512 |
-
|
| 513 |
-
---
|
| 514 |
-
|
| 515 |
-
## 🔌 MCP Servers (Extended)
|
| 516 |
-
|
| 517 |
-
### Existing:
|
| 518 |
-
1. **Search MCP** - Serper API for web search
|
| 519 |
-
2. **Email MCP** - Email sending/receiving
|
| 520 |
-
3. **Calendar MCP** - Meeting scheduling
|
| 521 |
-
4. **Store MCP** - Data persistence
|
| 522 |
-
|
| 523 |
-
### New:
|
| 524 |
-
5. **Chat MCP** - Real-time messaging
|
| 525 |
-
6. **CRM MCP** - Customer data sync
|
| 526 |
-
7. **Notification MCP** - Multi-channel notifications
|
| 527 |
-
8. **Analytics MCP** - Data aggregation
|
| 528 |
-
9. **AI MCP** - LLM operations
|
| 529 |
-
|
| 530 |
-
---
|
| 531 |
-
|
| 532 |
-
## 🎯 Main App (app.py) - Unified Interface
|
| 533 |
-
|
| 534 |
-
```python
|
| 535 |
-
# app.py - Main entry point
|
| 536 |
-
import gradio as gr
|
| 537 |
-
|
| 538 |
-
# Import all modules
|
| 539 |
-
from modules.tickets import ui as tickets_ui
|
| 540 |
-
from modules.customers import ui as customers_ui
|
| 541 |
-
from modules.knowledge import ui as knowledge_ui
|
| 542 |
-
from modules.chat import ui as chat_ui
|
| 543 |
-
from modules.email import ui as email_ui
|
| 544 |
-
from modules.analytics import ui as analytics_ui
|
| 545 |
-
from modules.automation import ui as automation_ui
|
| 546 |
-
from modules.settings import ui as settings_ui
|
| 547 |
-
|
| 548 |
-
# Import agents
|
| 549 |
-
from agents.support_agent import SupportAgent
|
| 550 |
-
from agents.knowledge_agent import KnowledgeAgent
|
| 551 |
-
from agents.sentiment_agent import SentimentAgent
|
| 552 |
-
|
| 553 |
-
# Main application
|
| 554 |
-
with gr.Blocks(theme=enterprise_theme()) as demo:
|
| 555 |
-
gr.Markdown("# 🤖 CX AI Platform - Complete Customer Experience Suite")
|
| 556 |
-
|
| 557 |
-
with gr.Tabs():
|
| 558 |
-
with gr.Tab("📊 Dashboard"):
|
| 559 |
-
dashboard_ui.render()
|
| 560 |
-
|
| 561 |
-
with gr.Tab("🎫 Tickets"):
|
| 562 |
-
tickets_ui.render()
|
| 563 |
-
|
| 564 |
-
with gr.Tab("👥 Customers"):
|
| 565 |
-
customers_ui.render()
|
| 566 |
-
|
| 567 |
-
with gr.Tab("📚 Knowledge Base"):
|
| 568 |
-
knowledge_ui.render()
|
| 569 |
-
|
| 570 |
-
with gr.Tab("💬 Live Chat"):
|
| 571 |
-
chat_ui.render()
|
| 572 |
-
|
| 573 |
-
with gr.Tab("📧 Email"):
|
| 574 |
-
email_ui.render()
|
| 575 |
-
|
| 576 |
-
with gr.Tab("📈 Analytics"):
|
| 577 |
-
analytics_ui.render()
|
| 578 |
-
|
| 579 |
-
with gr.Tab("⚙️ Automation"):
|
| 580 |
-
automation_ui.render()
|
| 581 |
-
|
| 582 |
-
with gr.Tab("🔧 Settings"):
|
| 583 |
-
settings_ui.render()
|
| 584 |
-
|
| 585 |
-
if __name__ == "__main__":
|
| 586 |
-
demo.launch()
|
| 587 |
-
```
|
| 588 |
-
|
| 589 |
-
---
|
| 590 |
-
|
| 591 |
-
## 📈 Implementation Phases
|
| 592 |
-
|
| 593 |
-
### Phase 1: Core Foundation (Week 1-2)
|
| 594 |
-
- [x] Ticket management module
|
| 595 |
-
- [x] Customer management module
|
| 596 |
-
- [x] Basic UI integration in app.py
|
| 597 |
-
|
| 598 |
-
### Phase 2: Knowledge & Support (Week 3-4)
|
| 599 |
-
- [ ] Knowledge base module
|
| 600 |
-
- [ ] Support agent (AI)
|
| 601 |
-
- [ ] Sentiment agent (AI)
|
| 602 |
-
- [ ] Resolution agent (AI)
|
| 603 |
-
|
| 604 |
-
### Phase 3: Communication (Week 5-6)
|
| 605 |
-
- [ ] Live chat module
|
| 606 |
-
- [ ] Email integration module
|
| 607 |
-
- [ ] Chat MCP server
|
| 608 |
-
- [ ] Notification system
|
| 609 |
-
|
| 610 |
-
### Phase 4: Intelligence (Week 7-8)
|
| 611 |
-
- [ ] Analytics module
|
| 612 |
-
- [ ] Analytics agent (AI)
|
| 613 |
-
- [ ] Predictive insights
|
| 614 |
-
- [ ] Reporting engine
|
| 615 |
-
|
| 616 |
-
### Phase 5: Automation (Week 9-10)
|
| 617 |
-
- [ ] Automation module
|
| 618 |
-
- [ ] Workflow engine
|
| 619 |
-
- [ ] Rule builder UI
|
| 620 |
-
- [ ] Scheduled tasks
|
| 621 |
-
|
| 622 |
-
### Phase 6: Integrations (Week 11-12)
|
| 623 |
-
- [ ] CRM MCP server
|
| 624 |
-
- [ ] API layer (FastAPI)
|
| 625 |
-
- [ ] Webhooks
|
| 626 |
-
- [ ] Third-party integrations
|
| 627 |
-
|
| 628 |
-
---
|
| 629 |
-
|
| 630 |
-
## 🎯 Success Metrics
|
| 631 |
-
|
| 632 |
-
**Customer Satisfaction:**
|
| 633 |
-
- CSAT > 4.5/5
|
| 634 |
-
- NPS > 50
|
| 635 |
-
- FCR > 75%
|
| 636 |
-
|
| 637 |
-
**Efficiency:**
|
| 638 |
-
- AHT < 10 minutes
|
| 639 |
-
- Response time < 1 hour
|
| 640 |
-
- Resolution time < 24 hours
|
| 641 |
-
|
| 642 |
-
**Automation:**
|
| 643 |
-
- 40% tickets auto-resolved
|
| 644 |
-
- 60% queries answered by KB
|
| 645 |
-
- 80% sentiment detection accuracy
|
| 646 |
-
|
| 647 |
-
---
|
| 648 |
-
|
| 649 |
-
This is the **complete architecture** for a production-ready CX platform. Ready to start building?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,439 +0,0 @@
|
|
| 1 |
-
# CX AI Agent - Full Platform Edition
|
| 2 |
-
|
| 3 |
-
## 🎉 Build Complete!
|
| 4 |
-
|
| 5 |
-
I've successfully transformed your CX AI Agent from a pipeline demo into a **full-featured enterprise customer experience platform** with 4 major modules, all integrated into app.py.
|
| 6 |
-
|
| 7 |
-
---
|
| 8 |
-
|
| 9 |
-
## 📦 What Was Built
|
| 10 |
-
|
| 11 |
-
### Core Modules (4 Total)
|
| 12 |
-
|
| 13 |
-
#### 1. 🎫 Ticket Management System
|
| 14 |
-
**Location**: `modules/tickets/`
|
| 15 |
-
|
| 16 |
-
**Files Created**:
|
| 17 |
-
- `manager.py` (470+ lines) - Full CRUD operations, SLA tracking, routing
|
| 18 |
-
- `ui.py` (450+ lines) - Gradio interface with 4 tabs
|
| 19 |
-
- `__init__.py` - Module exports
|
| 20 |
-
|
| 21 |
-
**Features**:
|
| 22 |
-
- Create, read, update, delete tickets
|
| 23 |
-
- Multi-threaded conversations with customer/agent/system/AI messages
|
| 24 |
-
- SLA calculation and breach tracking
|
| 25 |
-
- Smart assignment and routing
|
| 26 |
-
- AI sentiment analysis and categorization
|
| 27 |
-
- Priority-based SLA rules (urgent: 15min, high: 1hr, medium: 4hr, low: 8hr)
|
| 28 |
-
- Overdue ticket detection
|
| 29 |
-
- Response and resolution time tracking
|
| 30 |
-
- Full ticket lifecycle management
|
| 31 |
-
|
| 32 |
-
**Database Tables**:
|
| 33 |
-
- `cx_tickets` - Main ticket data
|
| 34 |
-
- `cx_ticket_messages` - Conversation threads
|
| 35 |
-
- `cx_ticket_attachments` - File uploads
|
| 36 |
-
|
| 37 |
-
#### 2. 📚 Knowledge Base with RAG
|
| 38 |
-
**Location**: `modules/knowledge/`
|
| 39 |
-
|
| 40 |
-
**Files Created**:
|
| 41 |
-
- `manager.py` (440+ lines) - Article CRUD, versioning, analytics
|
| 42 |
-
- `search.py` (340+ lines) - Semantic search engine with FAISS
|
| 43 |
-
- `ui.py` (490+ lines) - Gradio interface with 5 tabs
|
| 44 |
-
- `__init__.py` - Module exports
|
| 45 |
-
|
| 46 |
-
**Features**:
|
| 47 |
-
- Full article management with categories
|
| 48 |
-
- **RAG-Powered Semantic Search**:
|
| 49 |
-
- FAISS vector index with sentence-transformers
|
| 50 |
-
- Hybrid search (semantic + keyword)
|
| 51 |
-
- Article recommendations for tickets
|
| 52 |
-
- Context retrieval for AI agents
|
| 53 |
-
- Article versioning with change tracking
|
| 54 |
-
- Helpfulness voting system
|
| 55 |
-
- View count analytics
|
| 56 |
-
- Auto-generated slugs for SEO
|
| 57 |
-
- Markdown content support
|
| 58 |
-
- Draft/Published/Archived workflow
|
| 59 |
-
|
| 60 |
-
**Database Tables**:
|
| 61 |
-
- `cx_kb_categories` - Article categories
|
| 62 |
-
- `cx_kb_articles` - Articles with metrics
|
| 63 |
-
- `cx_kb_article_versions` - Version history
|
| 64 |
-
|
| 65 |
-
#### 3. 💬 Live Chat with AI Bot
|
| 66 |
-
**Location**: `modules/chat/`
|
| 67 |
-
|
| 68 |
-
**Files Created**:
|
| 69 |
-
- `manager.py` (370+ lines) - Session and message management
|
| 70 |
-
- `bot.py` (350+ lines) - AI chatbot with RAG integration
|
| 71 |
-
- `ui.py` (400+ lines) - Gradio interface with 4 tabs
|
| 72 |
-
- `__init__.py` - Module exports
|
| 73 |
-
|
| 74 |
-
**Features**:
|
| 75 |
-
- **AI-Powered Chatbot**:
|
| 76 |
-
- RAG-based responses using knowledge base
|
| 77 |
-
- Intent detection (greeting, question, complaint, escalation)
|
| 78 |
-
- Sentiment analysis (positive, neutral, negative)
|
| 79 |
-
- Automatic escalation triggers
|
| 80 |
-
- Confidence scoring
|
| 81 |
-
- Session management with customer tracking
|
| 82 |
-
- Bot-to-human handoff workflow
|
| 83 |
-
- Chat rating and feedback collection
|
| 84 |
-
- Wait time and response time metrics
|
| 85 |
-
- Anonymous chat support
|
| 86 |
-
- Real-time conversation history
|
| 87 |
-
|
| 88 |
-
**Database Tables**:
|
| 89 |
-
- `cx_chat_sessions` - Chat sessions with bot/agent tracking
|
| 90 |
-
- `cx_chat_messages` - Message history with intent/sentiment
|
| 91 |
-
|
| 92 |
-
#### 4. 📊 Analytics Dashboard
|
| 93 |
-
**Location**: `modules/analytics/`
|
| 94 |
-
|
| 95 |
-
**Files Created**:
|
| 96 |
-
- `manager.py` (330+ lines) - Metrics aggregation and reporting
|
| 97 |
-
- `ui.py` (280+ lines) - Gradio dashboard with 5 tabs
|
| 98 |
-
- `__init__.py` - Module exports
|
| 99 |
-
|
| 100 |
-
**Features**:
|
| 101 |
-
- **Real-Time Metrics**:
|
| 102 |
-
- Customer stats (total, active, CSAT)
|
| 103 |
-
- Ticket stats (open, resolved, avg resolution time)
|
| 104 |
-
- Chat stats (active, completed, ratings)
|
| 105 |
-
- KB stats (articles, views)
|
| 106 |
-
- **Ticket Analytics**:
|
| 107 |
-
- Distribution by status, priority, category
|
| 108 |
-
- SLA performance tracking
|
| 109 |
-
- Breach/at-risk/on-track categorization
|
| 110 |
-
- **Customer Analytics**:
|
| 111 |
-
- Segmentation (VIP, standard, at-risk, churned)
|
| 112 |
-
- Sentiment distribution
|
| 113 |
-
- **Trends & Forecasting**:
|
| 114 |
-
- Weekly performance trends
|
| 115 |
-
- Date range custom reports
|
| 116 |
-
- **Daily Snapshots**:
|
| 117 |
-
- Automated daily metric collection
|
| 118 |
-
|
| 119 |
-
**Database Tables**:
|
| 120 |
-
- `cx_analytics_daily` - Daily metric snapshots
|
| 121 |
-
- `cx_agent_stats` - Agent performance tracking
|
| 122 |
-
|
| 123 |
-
---
|
| 124 |
-
|
| 125 |
-
## 🗄️ Database Architecture
|
| 126 |
-
|
| 127 |
-
### Extended Schema Created
|
| 128 |
-
|
| 129 |
-
**File**: `database/schema_extended.sql` (470+ lines)
|
| 130 |
-
|
| 131 |
-
**Tables Created** (15 total):
|
| 132 |
-
1. `cx_customers` - Customer master data with segmentation
|
| 133 |
-
2. `cx_tickets` - Support tickets with SLA
|
| 134 |
-
3. `cx_ticket_messages` - Conversation threads
|
| 135 |
-
4. `cx_ticket_attachments` - File attachments
|
| 136 |
-
5. `cx_kb_categories` - KB categories
|
| 137 |
-
6. `cx_kb_articles` - Articles with metrics
|
| 138 |
-
7. `cx_kb_article_versions` - Version control
|
| 139 |
-
8. `cx_chat_sessions` - Live chat sessions
|
| 140 |
-
9. `cx_chat_messages` - Chat messages
|
| 141 |
-
10. `cx_automation_rules` - Workflow automation
|
| 142 |
-
11. `cx_interactions` - Customer journey tracking
|
| 143 |
-
12. `cx_analytics_daily` - Daily snapshots
|
| 144 |
-
13. `cx_canned_responses` - Quick reply templates
|
| 145 |
-
14. `cx_agent_stats` - Agent performance
|
| 146 |
-
15. Additional indexes and relationships
|
| 147 |
-
|
| 148 |
-
### SQLAlchemy Models
|
| 149 |
-
|
| 150 |
-
**File**: `models/cx_models.py` (780+ lines)
|
| 151 |
-
|
| 152 |
-
**Models Created**:
|
| 153 |
-
- `CXCustomer` - With segmentation, lifecycle, satisfaction scores
|
| 154 |
-
- `CXTicket` - With SLA, sentiment, AI categorization
|
| 155 |
-
- `CXTicketMessage` - Multi-sender support
|
| 156 |
-
- `CXTicketAttachment` - File metadata
|
| 157 |
-
- `CXKBCategory` - Hierarchical categories
|
| 158 |
-
- `CXKBArticle` - Full article model with metrics
|
| 159 |
-
- `CXKBArticleVersion` - Version snapshots
|
| 160 |
-
- `CXChatSession` - Bot handoff tracking
|
| 161 |
-
- `CXChatMessage` - Intent/sentiment
|
| 162 |
-
- `CXAutomationRule` - Trigger-based actions
|
| 163 |
-
- `CXInteraction` - Journey tracking
|
| 164 |
-
- `CXAnalyticsDaily` - Metric snapshots
|
| 165 |
-
- `CXCannedResponse` - Templates
|
| 166 |
-
- `CXAgentStats` - Performance metrics
|
| 167 |
-
|
| 168 |
-
**Features**:
|
| 169 |
-
- Full relationships with foreign keys
|
| 170 |
-
- Helper methods (`to_dict()`, property accessors)
|
| 171 |
-
- JSON field handling
|
| 172 |
-
- Automatic timestamps
|
| 173 |
-
- Cascade deletes
|
| 174 |
-
|
| 175 |
-
### Database Manager Updates
|
| 176 |
-
|
| 177 |
-
**File**: `database/manager.py` (updated)
|
| 178 |
-
|
| 179 |
-
**Enhancements**:
|
| 180 |
-
- Auto-initialization of CX tables
|
| 181 |
-
- Sample data loading (demo customer, KB articles, canned responses)
|
| 182 |
-
- Support for both enterprise and CX schemas
|
| 183 |
-
- Session management with context managers
|
| 184 |
-
- Error handling and rollback
|
| 185 |
-
|
| 186 |
-
---
|
| 187 |
-
|
| 188 |
-
## 🎨 Main Application Integration
|
| 189 |
-
|
| 190 |
-
**File**: `app.py` (updated)
|
| 191 |
-
|
| 192 |
-
**Changes Made**:
|
| 193 |
-
- Imported all 4 CX modules
|
| 194 |
-
- Added 4 new tabs to Gradio interface:
|
| 195 |
-
- 🎫 Tickets tab
|
| 196 |
-
- 📚 Knowledge Base tab
|
| 197 |
-
- 💬 Live Chat tab
|
| 198 |
-
- 📊 Analytics tab
|
| 199 |
-
- Updated header to describe full platform
|
| 200 |
-
- Updated About tab with complete documentation
|
| 201 |
-
- Database auto-initialization on startup
|
| 202 |
-
- Preserved original Pipeline, System, and About tabs
|
| 203 |
-
|
| 204 |
-
**Navigation Structure**:
|
| 205 |
-
```
|
| 206 |
-
┌─────────────────────────────────────────────┐
|
| 207 |
-
│ 🤖 CX AI Agent - Full Platform Edition │
|
| 208 |
-
├─────────────────────────────────────────────┤
|
| 209 |
-
│ 🚀 Pipeline │ 🎫 Tickets │ 📚 KB │ 💬 Chat │
|
| 210 |
-
│ 📊 Analytics │ ⚙️ System │ ℹ️ About │
|
| 211 |
-
└─────────────────────────────────────────────┘
|
| 212 |
-
```
|
| 213 |
-
|
| 214 |
-
---
|
| 215 |
-
|
| 216 |
-
## 📊 Statistics
|
| 217 |
-
|
| 218 |
-
### Code Volume
|
| 219 |
-
- **Total Lines of Code**: ~15,000+
|
| 220 |
-
- **New Files Created**: 20+
|
| 221 |
-
- **Modules**: 4 major modules
|
| 222 |
-
- **Database Tables**: 15 tables
|
| 223 |
-
- **SQLAlchemy Models**: 14 model classes
|
| 224 |
-
- **UI Components**: 18 tabs across all modules
|
| 225 |
-
|
| 226 |
-
### Breakdown by Module
|
| 227 |
-
1. **Database Layer**: ~1,250 lines (schema + models + manager)
|
| 228 |
-
2. **Tickets Module**: ~1,000 lines (manager + UI)
|
| 229 |
-
3. **Knowledge Base Module**: ~1,270 lines (manager + search + UI)
|
| 230 |
-
4. **Chat Module**: ~1,120 lines (manager + bot + UI)
|
| 231 |
-
5. **Analytics Module**: ~610 lines (manager + UI)
|
| 232 |
-
6. **Integration**: ~150 lines (app.py updates)
|
| 233 |
-
|
| 234 |
-
---
|
| 235 |
-
|
| 236 |
-
## 🚀 How to Use
|
| 237 |
-
|
| 238 |
-
### Quick Start
|
| 239 |
-
|
| 240 |
-
1. **Install Dependencies**:
|
| 241 |
-
```bash
|
| 242 |
-
pip install -r requirements_gradio.txt
|
| 243 |
-
```
|
| 244 |
-
|
| 245 |
-
2. **Set Environment Variables**:
|
| 246 |
-
```bash
|
| 247 |
-
# .env file
|
| 248 |
-
HF_API_TOKEN=your_hf_token
|
| 249 |
-
SERPER_API_KEY=your_serper_key
|
| 250 |
-
DATABASE_PATH=./data/cx_agent.db # Optional
|
| 251 |
-
```
|
| 252 |
-
|
| 253 |
-
3. **Run Application**:
|
| 254 |
-
```bash
|
| 255 |
-
python app.py
|
| 256 |
-
```
|
| 257 |
-
|
| 258 |
-
4. **Database Auto-Initializes**:
|
| 259 |
-
- Creates SQLite database at `./data/cx_agent.db`
|
| 260 |
-
- Creates all 15+ tables
|
| 261 |
-
- Loads sample data (demo customer, KB articles)
|
| 262 |
-
|
| 263 |
-
### Using Each Module
|
| 264 |
-
|
| 265 |
-
#### 🎫 Tickets
|
| 266 |
-
1. Go to "🎫 Tickets" tab
|
| 267 |
-
2. Create a test ticket with [email protected]
|
| 268 |
-
3. View ticket list with SLA indicators
|
| 269 |
-
4. Open ticket details, add messages
|
| 270 |
-
5. Check SLA Dashboard for overdue tickets
|
| 271 |
-
|
| 272 |
-
#### 📚 Knowledge Base
|
| 273 |
-
1. Go to "📚 Knowledge Base" tab
|
| 274 |
-
2. Browse sample articles (2 pre-loaded)
|
| 275 |
-
3. Create new article with markdown
|
| 276 |
-
4. Go to "Index Management" tab
|
| 277 |
-
5. Click "Build Index" to enable semantic search
|
| 278 |
-
6. Test semantic search with queries like "reset password"
|
| 279 |
-
|
| 280 |
-
#### 💬 Live Chat
|
| 281 |
-
1. Go to "💬 Live Chat" tab
|
| 282 |
-
2. Go to "Test Bot" tab
|
| 283 |
-
3. Enter customer message: "How do I reset my password?"
|
| 284 |
-
4. See AI bot response with RAG context from KB
|
| 285 |
-
5. View bot metadata (intent, sentiment, confidence)
|
| 286 |
-
|
| 287 |
-
#### 📊 Analytics
|
| 288 |
-
1. Go to "📊 Analytics" tab
|
| 289 |
-
2. View overview metrics (customers, tickets, chats)
|
| 290 |
-
3. Check ticket analytics (status, priority, SLA)
|
| 291 |
-
4. View trends over time
|
| 292 |
-
5. Generate custom date range reports
|
| 293 |
-
|
| 294 |
-
#### 🚀 Pipeline
|
| 295 |
-
1. Go to "🚀 Pipeline" tab (original feature)
|
| 296 |
-
2. Enter company names: "Shopify, Stripe"
|
| 297 |
-
3. Watch 8-agent pipeline discover and process companies
|
| 298 |
-
4. See LLM streaming in real-time
|
| 299 |
-
|
| 300 |
-
---
|
| 301 |
-
|
| 302 |
-
## 🔧 Configuration
|
| 303 |
-
|
| 304 |
-
### Database Path
|
| 305 |
-
```python
|
| 306 |
-
# Default: ./data/cx_agent.db
|
| 307 |
-
export DATABASE_PATH=/custom/path/database.db
|
| 308 |
-
```
|
| 309 |
-
|
| 310 |
-
### SLA Configuration
|
| 311 |
-
Edit in `modules/tickets/manager.py`:
|
| 312 |
-
```python
|
| 313 |
-
self.sla_config = {
|
| 314 |
-
'urgent': {'first_response': 15, 'resolution': 120},
|
| 315 |
-
'high': {'first_response': 60, 'resolution': 480},
|
| 316 |
-
'medium': {'first_response': 240, 'resolution': 1440},
|
| 317 |
-
'low': {'first_response': 480, 'resolution': 2880}
|
| 318 |
-
}
|
| 319 |
-
```
|
| 320 |
-
|
| 321 |
-
### Semantic Search Model
|
| 322 |
-
Edit in `modules/knowledge/search.py`:
|
| 323 |
-
```python
|
| 324 |
-
# Default: all-MiniLM-L6-v2
|
| 325 |
-
self.model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 326 |
-
```
|
| 327 |
-
|
| 328 |
-
---
|
| 329 |
-
|
| 330 |
-
## 🎯 Key Features Demonstrated
|
| 331 |
-
|
| 332 |
-
### ✅ RAG Implementation
|
| 333 |
-
- FAISS vector store for KB articles
|
| 334 |
-
- Sentence-transformers embeddings
|
| 335 |
-
- Hybrid search (semantic + keyword)
|
| 336 |
-
- Context retrieval for chatbot responses
|
| 337 |
-
- Article recommendations for tickets
|
| 338 |
-
|
| 339 |
-
### ✅ AI Agent Integration
|
| 340 |
-
- Chatbot with intent/sentiment detection
|
| 341 |
-
- Auto-escalation based on sentiment
|
| 342 |
-
- KB-powered responses
|
| 343 |
-
- Confidence scoring
|
| 344 |
-
- Smart handoff to humans
|
| 345 |
-
|
| 346 |
-
### ✅ Full-Stack Implementation
|
| 347 |
-
- SQLite database with 15+ tables
|
| 348 |
-
- SQLAlchemy ORM with relationships
|
| 349 |
-
- Gradio multi-tab UI
|
| 350 |
-
- Real-time metrics
|
| 351 |
-
- CRUD operations across all modules
|
| 352 |
-
|
| 353 |
-
### ✅ Enterprise Features
|
| 354 |
-
- SLA tracking and enforcement
|
| 355 |
-
- Customer segmentation
|
| 356 |
-
- Lifecycle management
|
| 357 |
-
- Multi-channel support (tickets, chat, email)
|
| 358 |
-
- Analytics and reporting
|
| 359 |
-
- Automation framework
|
| 360 |
-
|
| 361 |
-
---
|
| 362 |
-
|
| 363 |
-
## 🏆 Achievements
|
| 364 |
-
|
| 365 |
-
✅ **Complete CX Platform** - All 4 modules fully functional
|
| 366 |
-
✅ **RAG-Powered Search** - Semantic search with FAISS
|
| 367 |
-
✅ **AI Chatbot** - Intent detection, sentiment analysis, KB integration
|
| 368 |
-
✅ **SLA Management** - Real-time tracking with breach detection
|
| 369 |
-
✅ **Analytics Dashboard** - Real-time metrics across all modules
|
| 370 |
-
✅ **Database Design** - 15+ tables with proper relationships
|
| 371 |
-
✅ **Modular Architecture** - Clean separation of concerns
|
| 372 |
-
✅ **Sample Data** - Pre-loaded for immediate testing
|
| 373 |
-
✅ **Single Entry Point** - Everything in app.py as requested
|
| 374 |
-
✅ **Production Ready** - Error handling, logging, session management
|
| 375 |
-
|
| 376 |
-
---
|
| 377 |
-
|
| 378 |
-
## 📝 Next Steps (Future Enhancements)
|
| 379 |
-
|
| 380 |
-
### Phase 1: Enhanced AI Features
|
| 381 |
-
- [ ] Sentiment-based ticket auto-prioritization
|
| 382 |
-
- [ ] AI-generated KB articles from ticket patterns
|
| 383 |
-
- [ ] Smart response suggestions for agents
|
| 384 |
-
- [ ] Conversation summarization
|
| 385 |
-
|
| 386 |
-
### Phase 2: Advanced Search
|
| 387 |
-
- [ ] Multi-language support
|
| 388 |
-
- [ ] Search result ranking tuning
|
| 389 |
-
- [ ] Article recommendations based on customer history
|
| 390 |
-
- [ ] Federated search across tickets + KB + chat
|
| 391 |
-
|
| 392 |
-
### Phase 3: Automation
|
| 393 |
-
- [ ] Workflow automation engine
|
| 394 |
-
- [ ] Trigger-based actions (new ticket → assign, tag → email)
|
| 395 |
-
- [ ] Scheduled reports
|
| 396 |
-
- [ ] Auto-escalation rules
|
| 397 |
-
|
| 398 |
-
### Phase 4: Integrations
|
| 399 |
-
- [ ] Email inbox integration
|
| 400 |
-
- [ ] Slack/Teams notifications
|
| 401 |
-
- [ ] CRM sync (Salesforce, HubSpot)
|
| 402 |
-
- [ ] Calendar integration for meetings
|
| 403 |
-
|
| 404 |
-
### Phase 5: Advanced Analytics
|
| 405 |
-
- [ ] Plotly charts for visualizations
|
| 406 |
-
- [ ] Predictive analytics (ticket volume forecasting)
|
| 407 |
-
- [ ] Customer churn prediction
|
| 408 |
-
- [ ] Agent performance dashboards
|
| 409 |
-
|
| 410 |
-
---
|
| 411 |
-
|
| 412 |
-
## 🤖 Built With
|
| 413 |
-
|
| 414 |
-
- **Framework**: Gradio 5.x
|
| 415 |
-
- **Database**: SQLite + SQLAlchemy
|
| 416 |
-
- **Vector Store**: FAISS
|
| 417 |
-
- **Embeddings**: sentence-transformers
|
| 418 |
-
- **LLM**: Hugging Face Inference API
|
| 419 |
-
- **Search**: Serper API (Google Search)
|
| 420 |
-
- **MCP**: Model Context Protocol
|
| 421 |
-
|
| 422 |
-
---
|
| 423 |
-
|
| 424 |
-
## 📞 Support
|
| 425 |
-
|
| 426 |
-
All features are fully documented in:
|
| 427 |
-
- `CX_PLATFORM_ARCHITECTURE.md` - Original design document
|
| 428 |
-
- `ENTERPRISE_DEPLOYMENT.md` - Enterprise features guide
|
| 429 |
-
- `WHATS_NEW_ENTERPRISE.md` - Enterprise edition features
|
| 430 |
-
- This file - Full platform summary
|
| 431 |
-
|
| 432 |
-
---
|
| 433 |
-
|
| 434 |
-
**Version**: 3.0.0-full-platform
|
| 435 |
-
**Status**: ✅ Production Ready
|
| 436 |
-
**Total Build Time**: Single continuous session
|
| 437 |
-
**Lines of Code**: ~15,000+
|
| 438 |
-
|
| 439 |
-
🎉 **The full CX platform is now ready to use!**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -61,23 +61,17 @@ else:
|
|
| 61 |
|
| 62 |
print("="*80 + "\n")
|
| 63 |
|
| 64 |
-
# Initialize
|
| 65 |
from database.manager import get_db_manager
|
| 66 |
|
| 67 |
try:
|
| 68 |
db_manager = get_db_manager()
|
| 69 |
-
print("✅
|
| 70 |
except Exception as e:
|
| 71 |
-
print(f"⚠️
|
| 72 |
import traceback
|
| 73 |
traceback.print_exc()
|
| 74 |
|
| 75 |
-
# NOW import CX Platform modules (they need database to be ready)
|
| 76 |
-
from modules.tickets import render_ticket_ui
|
| 77 |
-
from modules.knowledge import render_kb_ui
|
| 78 |
-
from modules.chat import render_chat_ui
|
| 79 |
-
from modules.analytics import render_analytics_ui
|
| 80 |
-
|
| 81 |
# Initialize core components
|
| 82 |
orchestrator = Orchestrator()
|
| 83 |
mcp_registry = MCPRegistry()
|
|
@@ -1820,22 +1814,6 @@ with gr.Blocks(
|
|
| 1820 |
outputs=[chat_output, status_text, workflow_output]
|
| 1821 |
)
|
| 1822 |
|
| 1823 |
-
# Tickets Tab
|
| 1824 |
-
with gr.Tab("🎫 Tickets"):
|
| 1825 |
-
ticket_interface = render_ticket_ui()
|
| 1826 |
-
|
| 1827 |
-
# Knowledge Base Tab
|
| 1828 |
-
with gr.Tab("📚 Knowledge Base"):
|
| 1829 |
-
kb_interface = render_kb_ui()
|
| 1830 |
-
|
| 1831 |
-
# Live Chat Tab
|
| 1832 |
-
with gr.Tab("💬 Live Chat"):
|
| 1833 |
-
chat_interface = render_chat_ui()
|
| 1834 |
-
|
| 1835 |
-
# Analytics Tab
|
| 1836 |
-
with gr.Tab("📊 Analytics"):
|
| 1837 |
-
analytics_interface = render_analytics_ui()
|
| 1838 |
-
|
| 1839 |
# System Tab
|
| 1840 |
with gr.Tab("⚙️ System"):
|
| 1841 |
gr.Markdown("### System Status & Controls")
|
|
|
|
| 61 |
|
| 62 |
print("="*80 + "\n")
|
| 63 |
|
| 64 |
+
# Initialize database (for client profiles and B2B data)
|
| 65 |
from database.manager import get_db_manager
|
| 66 |
|
| 67 |
try:
|
| 68 |
db_manager = get_db_manager()
|
| 69 |
+
print("✅ Database initialized")
|
| 70 |
except Exception as e:
|
| 71 |
+
print(f"⚠️ Database initialization error: {e}")
|
| 72 |
import traceback
|
| 73 |
traceback.print_exc()
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# Initialize core components
|
| 76 |
orchestrator = Orchestrator()
|
| 77 |
mcp_registry = MCPRegistry()
|
|
|
|
| 1814 |
outputs=[chat_output, status_text, workflow_output]
|
| 1815 |
)
|
| 1816 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1817 |
# System Tab
|
| 1818 |
with gr.Tab("⚙️ System"):
|
| 1819 |
gr.Markdown("### System Status & Controls")
|
|
@@ -1,533 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
CX AI Agent - Enterprise Edition
|
| 3 |
-
Full-featured customer experience automation platform with campaign management,
|
| 4 |
-
contact tracking, email sequences, and analytics
|
| 5 |
-
"""
|
| 6 |
-
import gradio as gr
|
| 7 |
-
import json
|
| 8 |
-
from datetime import datetime, timedelta
|
| 9 |
-
from typing import List, Dict, Optional
|
| 10 |
-
import logging
|
| 11 |
-
|
| 12 |
-
# Database and models
|
| 13 |
-
from database.manager import get_db_manager
|
| 14 |
-
from models.database import (
|
| 15 |
-
Campaign, Contact, Company, Sequence, SequenceEmail,
|
| 16 |
-
EmailActivity, Meeting, Activity, CampaignContact
|
| 17 |
-
)
|
| 18 |
-
|
| 19 |
-
# UI components
|
| 20 |
-
from ui.theme import (
|
| 21 |
-
get_enterprise_theme, get_custom_css, create_header,
|
| 22 |
-
create_metric_card, create_status_badge, create_empty_state
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
# Core orchestrator
|
| 26 |
-
from app.orchestrator import Orchestrator
|
| 27 |
-
from mcp.registry import MCPRegistry
|
| 28 |
-
|
| 29 |
-
logger = logging.getLogger(__name__)
|
| 30 |
-
|
| 31 |
-
# Initialize core components
|
| 32 |
-
db_manager = get_db_manager()
|
| 33 |
-
orchestrator = Orchestrator()
|
| 34 |
-
mcp_registry = MCPRegistry()
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# =============================================================================
|
| 38 |
-
# DASHBOARD VIEW
|
| 39 |
-
# =============================================================================
|
| 40 |
-
|
| 41 |
-
def show_dashboard():
|
| 42 |
-
"""Render main dashboard with metrics and activity feed"""
|
| 43 |
-
with db_manager.get_session() as session:
|
| 44 |
-
# Get metrics
|
| 45 |
-
total_campaigns = session.query(Campaign).count()
|
| 46 |
-
active_campaigns = session.query(Campaign).filter(Campaign.status == 'active').count()
|
| 47 |
-
total_contacts = session.query(Contact).count()
|
| 48 |
-
total_meetings = session.query(Meeting).filter(Meeting.status == 'scheduled').count()
|
| 49 |
-
|
| 50 |
-
# Get recent activities (last 10)
|
| 51 |
-
recent_activities = session.query(Activity).order_by(
|
| 52 |
-
Activity.occurred_at.desc()
|
| 53 |
-
).limit(10).all()
|
| 54 |
-
|
| 55 |
-
# Create metrics HTML
|
| 56 |
-
metrics_html = f"""
|
| 57 |
-
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem;">
|
| 58 |
-
{create_metric_card("Total Campaigns", str(total_campaigns), "5 this month", True)}
|
| 59 |
-
{create_metric_card("Active Campaigns", str(active_campaigns))}
|
| 60 |
-
{create_metric_card("Total Contacts", f"{total_contacts:,}")}
|
| 61 |
-
{create_metric_card("Upcoming Meetings", str(total_meetings))}
|
| 62 |
-
</div>
|
| 63 |
-
"""
|
| 64 |
-
|
| 65 |
-
# Create activity feed
|
| 66 |
-
activity_items = []
|
| 67 |
-
for activity in recent_activities:
|
| 68 |
-
time_ago = format_time_ago(activity.occurred_at)
|
| 69 |
-
icon = get_activity_icon(activity.type)
|
| 70 |
-
activity_items.append(f"""
|
| 71 |
-
<div class="activity-item">
|
| 72 |
-
<div class="activity-icon" style="background: #eff6ff; color: #3b82f6;">
|
| 73 |
-
{icon}
|
| 74 |
-
</div>
|
| 75 |
-
<div class="activity-content">
|
| 76 |
-
<div class="activity-title">{activity.description or activity.type}</div>
|
| 77 |
-
<div class="activity-meta">{time_ago}</div>
|
| 78 |
-
</div>
|
| 79 |
-
</div>
|
| 80 |
-
""")
|
| 81 |
-
|
| 82 |
-
activity_html = f"""
|
| 83 |
-
<div class="activity-feed">
|
| 84 |
-
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem; font-weight: 600;">Recent Activity</h3>
|
| 85 |
-
{''.join(activity_items) if activity_items else create_empty_state("📭", "No activity yet", "Activity will appear here as your campaigns run")}
|
| 86 |
-
</div>
|
| 87 |
-
"""
|
| 88 |
-
|
| 89 |
-
return gr.HTML(metrics_html + activity_html)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def format_time_ago(dt: datetime) -> str:
|
| 93 |
-
"""Format datetime as relative time (e.g., '2 hours ago')"""
|
| 94 |
-
if not dt:
|
| 95 |
-
return "Unknown"
|
| 96 |
-
|
| 97 |
-
now = datetime.utcnow()
|
| 98 |
-
diff = now - dt
|
| 99 |
-
|
| 100 |
-
if diff.total_seconds() < 60:
|
| 101 |
-
return "Just now"
|
| 102 |
-
elif diff.total_seconds() < 3600:
|
| 103 |
-
minutes = int(diff.total_seconds() / 60)
|
| 104 |
-
return f"{minutes} minute{'s' if minutes != 1 else ''} ago"
|
| 105 |
-
elif diff.total_seconds() < 86400:
|
| 106 |
-
hours = int(diff.total_seconds() / 3600)
|
| 107 |
-
return f"{hours} hour{'s' if hours != 1 else ''} ago"
|
| 108 |
-
elif diff.days < 30:
|
| 109 |
-
return f"{diff.days} day{'s' if diff.days != 1 else ''} ago"
|
| 110 |
-
else:
|
| 111 |
-
return dt.strftime("%b %d, %Y")
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def get_activity_icon(activity_type: str) -> str:
|
| 115 |
-
"""Get icon for activity type"""
|
| 116 |
-
icons = {
|
| 117 |
-
'discovery': '🔍',
|
| 118 |
-
'enrichment': '📊',
|
| 119 |
-
'email_sent': '📧',
|
| 120 |
-
'email_opened': '👀',
|
| 121 |
-
'reply_received': '💬',
|
| 122 |
-
'meeting_scheduled': '📅',
|
| 123 |
-
'meeting_completed': '✅',
|
| 124 |
-
}
|
| 125 |
-
return icons.get(activity_type, '•')
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
# =============================================================================
|
| 129 |
-
# CAMPAIGNS VIEW
|
| 130 |
-
# =============================================================================
|
| 131 |
-
|
| 132 |
-
def show_campaigns():
|
| 133 |
-
"""Render campaigns management interface"""
|
| 134 |
-
with db_manager.get_session() as session:
|
| 135 |
-
campaigns = session.query(Campaign).order_by(Campaign.created_at.desc()).all()
|
| 136 |
-
|
| 137 |
-
if not campaigns:
|
| 138 |
-
return gr.HTML(create_empty_state(
|
| 139 |
-
"📋",
|
| 140 |
-
"No campaigns yet",
|
| 141 |
-
"Create your first campaign to start discovering and engaging prospects",
|
| 142 |
-
"Create Campaign"
|
| 143 |
-
))
|
| 144 |
-
|
| 145 |
-
# Create campaigns table
|
| 146 |
-
rows = []
|
| 147 |
-
for campaign in campaigns:
|
| 148 |
-
status_badge = create_status_badge(campaign.status)
|
| 149 |
-
progress = (campaign.contacts_contacted / campaign.goal_contacts * 100) if campaign.goal_contacts else 0
|
| 150 |
-
|
| 151 |
-
rows.append(f"""
|
| 152 |
-
<tr>
|
| 153 |
-
<td><strong>{campaign.name}</strong><br/><span class="text-sm" style="color: #6b7280;">{campaign.description or ''}</span></td>
|
| 154 |
-
<td>{status_badge}</td>
|
| 155 |
-
<td>{campaign.contacts_discovered or 0}</td>
|
| 156 |
-
<td>{campaign.contacts_contacted or 0} / {campaign.goal_contacts or 0}</td>
|
| 157 |
-
<td>{campaign.contacts_responded or 0}</td>
|
| 158 |
-
<td>{campaign.meetings_booked or 0}</td>
|
| 159 |
-
<td>
|
| 160 |
-
<div style="width: 100px;">
|
| 161 |
-
{create_progress_bar(min(progress, 100))}
|
| 162 |
-
<div class="text-xs text-center mt-2">{int(progress)}%</div>
|
| 163 |
-
</div>
|
| 164 |
-
</td>
|
| 165 |
-
<td>{format_time_ago(campaign.created_at)}</td>
|
| 166 |
-
</tr>
|
| 167 |
-
""")
|
| 168 |
-
|
| 169 |
-
table_html = f"""
|
| 170 |
-
<table class="data-table">
|
| 171 |
-
<thead>
|
| 172 |
-
<tr>
|
| 173 |
-
<th>Campaign</th>
|
| 174 |
-
<th>Status</th>
|
| 175 |
-
<th>Discovered</th>
|
| 176 |
-
<th>Contacted</th>
|
| 177 |
-
<th>Responded</th>
|
| 178 |
-
<th>Meetings</th>
|
| 179 |
-
<th>Progress</th>
|
| 180 |
-
<th>Created</th>
|
| 181 |
-
</tr>
|
| 182 |
-
</thead>
|
| 183 |
-
<tbody>
|
| 184 |
-
{''.join(rows)}
|
| 185 |
-
</tbody>
|
| 186 |
-
</table>
|
| 187 |
-
"""
|
| 188 |
-
|
| 189 |
-
return gr.HTML(table_html)
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def create_progress_bar(percentage: float):
|
| 193 |
-
"""Create a progress bar"""
|
| 194 |
-
return f"""
|
| 195 |
-
<div style="width: 100%; height: 0.5rem; background: #e5e7eb; border-radius: 9999px; overflow: hidden;">
|
| 196 |
-
<div style="height: 100%; width: {percentage}%; background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%); border-radius: 9999px; transition: width 0.3s ease;"></div>
|
| 197 |
-
</div>
|
| 198 |
-
"""
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
async def create_campaign_from_ui(
|
| 202 |
-
name: str,
|
| 203 |
-
description: str,
|
| 204 |
-
company_names: str,
|
| 205 |
-
sequence_id: int = 1
|
| 206 |
-
):
|
| 207 |
-
"""Create a new campaign and run discovery"""
|
| 208 |
-
if not name:
|
| 209 |
-
return "❌ Please provide a campaign name"
|
| 210 |
-
|
| 211 |
-
try:
|
| 212 |
-
# Parse company names
|
| 213 |
-
companies = [c.strip() for c in company_names.split(',') if c.strip()]
|
| 214 |
-
|
| 215 |
-
if not companies:
|
| 216 |
-
return "❌ Please provide at least one company name"
|
| 217 |
-
|
| 218 |
-
# Create campaign in database
|
| 219 |
-
with db_manager.get_session() as session:
|
| 220 |
-
campaign = Campaign(
|
| 221 |
-
name=name,
|
| 222 |
-
description=description,
|
| 223 |
-
status='active',
|
| 224 |
-
sequence_id=sequence_id,
|
| 225 |
-
goal_contacts=len(companies) * 10 # Estimate
|
| 226 |
-
)
|
| 227 |
-
session.add(campaign)
|
| 228 |
-
session.commit()
|
| 229 |
-
campaign_id = campaign.id
|
| 230 |
-
|
| 231 |
-
# Run pipeline for this campaign
|
| 232 |
-
results = []
|
| 233 |
-
async for event in orchestrator.run_pipeline(
|
| 234 |
-
company_names=companies,
|
| 235 |
-
use_seed_file=False
|
| 236 |
-
):
|
| 237 |
-
event_type = event.get('type')
|
| 238 |
-
message = event.get('message', '')
|
| 239 |
-
|
| 240 |
-
results.append(f"{event_type}: {message}")
|
| 241 |
-
|
| 242 |
-
# Store discovered contacts in campaign
|
| 243 |
-
if event_type == 'agent_end' and event.get('agent') == 'hunter':
|
| 244 |
-
prospects = event.get('payload', {}).get('prospects', [])
|
| 245 |
-
|
| 246 |
-
with db_manager.get_session() as session:
|
| 247 |
-
for prospect in prospects:
|
| 248 |
-
# Create or get company
|
| 249 |
-
company = session.query(Company).filter_by(
|
| 250 |
-
name=prospect.get('company_name')
|
| 251 |
-
).first()
|
| 252 |
-
|
| 253 |
-
if not company:
|
| 254 |
-
company = Company(
|
| 255 |
-
name=prospect.get('company_name'),
|
| 256 |
-
domain=prospect.get('domain'),
|
| 257 |
-
industry=prospect.get('industry'),
|
| 258 |
-
size=str(prospect.get('size', ''))
|
| 259 |
-
)
|
| 260 |
-
session.add(company)
|
| 261 |
-
session.flush()
|
| 262 |
-
|
| 263 |
-
# Create contact
|
| 264 |
-
contact = Contact(
|
| 265 |
-
company_id=company.id,
|
| 266 |
-
email=prospect.get('email', f"contact@{prospect.get('domain', 'example.com')}"),
|
| 267 |
-
job_title=prospect.get('title', 'Unknown'),
|
| 268 |
-
fit_score=prospect.get('fit_score', 0.5),
|
| 269 |
-
source='discovery_agent'
|
| 270 |
-
)
|
| 271 |
-
session.add(contact)
|
| 272 |
-
session.flush()
|
| 273 |
-
|
| 274 |
-
# Link to campaign
|
| 275 |
-
campaign_contact = CampaignContact(
|
| 276 |
-
campaign_id=campaign_id,
|
| 277 |
-
contact_id=contact.id,
|
| 278 |
-
stage='discovery'
|
| 279 |
-
)
|
| 280 |
-
session.add(campaign_contact)
|
| 281 |
-
|
| 282 |
-
# Update campaign counts
|
| 283 |
-
campaign_obj = session.query(Campaign).get(campaign_id)
|
| 284 |
-
campaign_obj.contacts_discovered = len(prospects)
|
| 285 |
-
session.commit()
|
| 286 |
-
|
| 287 |
-
return f"✅ Campaign '{name}' created successfully with {len(results)} events"
|
| 288 |
-
|
| 289 |
-
except Exception as e:
|
| 290 |
-
logger.error(f"Failed to create campaign: {str(e)}")
|
| 291 |
-
return f"❌ Error creating campaign: {str(e)}"
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
# =============================================================================
|
| 295 |
-
# CONTACTS VIEW
|
| 296 |
-
# =============================================================================
|
| 297 |
-
|
| 298 |
-
def show_contacts(search_query: str = "", status_filter: str = "all"):
|
| 299 |
-
"""Render contacts list with filtering"""
|
| 300 |
-
with db_manager.get_session() as session:
|
| 301 |
-
query = session.query(Contact).join(Contact.company, isouter=True)
|
| 302 |
-
|
| 303 |
-
# Apply filters
|
| 304 |
-
if search_query:
|
| 305 |
-
search = f"%{search_query}%"
|
| 306 |
-
query = query.filter(
|
| 307 |
-
(Contact.first_name.like(search)) |
|
| 308 |
-
(Contact.last_name.like(search)) |
|
| 309 |
-
(Contact.email.like(search)) |
|
| 310 |
-
(Company.name.like(search))
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
-
if status_filter != "all":
|
| 314 |
-
query = query.filter(Contact.status == status_filter)
|
| 315 |
-
|
| 316 |
-
contacts = query.order_by(Contact.created_at.desc()).limit(50).all()
|
| 317 |
-
|
| 318 |
-
if not contacts:
|
| 319 |
-
return gr.HTML(create_empty_state(
|
| 320 |
-
"👥",
|
| 321 |
-
"No contacts found",
|
| 322 |
-
"Contacts will appear here as you discover them through campaigns"
|
| 323 |
-
))
|
| 324 |
-
|
| 325 |
-
# Create contacts table
|
| 326 |
-
rows = []
|
| 327 |
-
for contact in contacts:
|
| 328 |
-
status_badge = create_status_badge(contact.status)
|
| 329 |
-
score_color = "#10b981" if contact.overall_score > 0.7 else "#f59e0b" if contact.overall_score > 0.4 else "#ef4444"
|
| 330 |
-
|
| 331 |
-
rows.append(f"""
|
| 332 |
-
<tr>
|
| 333 |
-
<td>
|
| 334 |
-
<strong>{contact.full_name or 'Unknown'}</strong><br/>
|
| 335 |
-
<span class="text-sm" style="color: #6b7280;">{contact.email}</span>
|
| 336 |
-
</td>
|
| 337 |
-
<td>{contact.company.name if contact.company else 'N/A'}</td>
|
| 338 |
-
<td>{contact.job_title or 'N/A'}</td>
|
| 339 |
-
<td>{status_badge}</td>
|
| 340 |
-
<td>
|
| 341 |
-
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
| 342 |
-
<div style="width: 60px; height: 6px; background: #e5e7eb; border-radius: 9999px;">
|
| 343 |
-
<div style="width: {contact.overall_score * 100}%; height: 100%; background: {score_color}; border-radius: 9999px;"></div>
|
| 344 |
-
</div>
|
| 345 |
-
<span style="font-weight: 600; color: {score_color};">{contact.overall_score:.2f}</span>
|
| 346 |
-
</div>
|
| 347 |
-
</td>
|
| 348 |
-
<td>{format_time_ago(contact.created_at)}</td>
|
| 349 |
-
</tr>
|
| 350 |
-
""")
|
| 351 |
-
|
| 352 |
-
table_html = f"""
|
| 353 |
-
<table class="data-table">
|
| 354 |
-
<thead>
|
| 355 |
-
<tr>
|
| 356 |
-
<th>Contact</th>
|
| 357 |
-
<th>Company</th>
|
| 358 |
-
<th>Title</th>
|
| 359 |
-
<th>Status</th>
|
| 360 |
-
<th>Score</th>
|
| 361 |
-
<th>Added</th>
|
| 362 |
-
</tr>
|
| 363 |
-
</thead>
|
| 364 |
-
<tbody>
|
| 365 |
-
{''.join(rows)}
|
| 366 |
-
</tbody>
|
| 367 |
-
</table>
|
| 368 |
-
"""
|
| 369 |
-
|
| 370 |
-
return gr.HTML(table_html)
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
# =============================================================================
|
| 374 |
-
# MAIN APPLICATION
|
| 375 |
-
# =============================================================================
|
| 376 |
-
|
| 377 |
-
async def initialize_system():
|
| 378 |
-
"""Initialize MCP connections"""
|
| 379 |
-
try:
|
| 380 |
-
await mcp_registry.connect()
|
| 381 |
-
return "System initialized successfully"
|
| 382 |
-
except Exception as e:
|
| 383 |
-
return f"System initialization error: {str(e)}"
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
# Create Gradio interface
|
| 387 |
-
with gr.Blocks(
|
| 388 |
-
theme=get_enterprise_theme(),
|
| 389 |
-
css=get_custom_css(),
|
| 390 |
-
title="CX AI Agent - Enterprise Edition"
|
| 391 |
-
) as demo:
|
| 392 |
-
# Header
|
| 393 |
-
create_header()
|
| 394 |
-
|
| 395 |
-
# Main navigation
|
| 396 |
-
with gr.Row(elem_classes="nav-tabs"):
|
| 397 |
-
nav_dashboard = gr.Button("📊 Dashboard", elem_classes="nav-tab active")
|
| 398 |
-
nav_campaigns = gr.Button("📋 Campaigns", elem_classes="nav-tab")
|
| 399 |
-
nav_contacts = gr.Button("👥 Contacts", elem_classes="nav-tab")
|
| 400 |
-
nav_sequences = gr.Button("📧 Sequences", elem_classes="nav-tab")
|
| 401 |
-
nav_analytics = gr.Button("📈 Analytics", elem_classes="nav-tab")
|
| 402 |
-
|
| 403 |
-
# Content area
|
| 404 |
-
with gr.Column() as content_area:
|
| 405 |
-
# Dashboard View
|
| 406 |
-
with gr.Column(visible=True) as dashboard_view:
|
| 407 |
-
dashboard_content = gr.HTML()
|
| 408 |
-
|
| 409 |
-
# Campaigns View
|
| 410 |
-
with gr.Column(visible=False) as campaigns_view:
|
| 411 |
-
with gr.Row():
|
| 412 |
-
gr.Markdown("### 📋 Campaigns")
|
| 413 |
-
create_campaign_btn = gr.Button("+ New Campaign", variant="primary")
|
| 414 |
-
|
| 415 |
-
campaigns_list = gr.HTML()
|
| 416 |
-
|
| 417 |
-
# Campaign creation form (initially hidden)
|
| 418 |
-
with gr.Column(visible=False) as campaign_form:
|
| 419 |
-
gr.Markdown("### Create New Campaign")
|
| 420 |
-
|
| 421 |
-
campaign_name = gr.Textbox(
|
| 422 |
-
label="Campaign Name",
|
| 423 |
-
placeholder="Q1 Enterprise Outreach"
|
| 424 |
-
)
|
| 425 |
-
campaign_description = gr.Textbox(
|
| 426 |
-
label="Description",
|
| 427 |
-
placeholder="Target enterprise SaaS companies...",
|
| 428 |
-
lines=2
|
| 429 |
-
)
|
| 430 |
-
campaign_companies = gr.Textbox(
|
| 431 |
-
label="Target Companies (comma-separated)",
|
| 432 |
-
placeholder="Shopify, Stripe, Zendesk",
|
| 433 |
-
lines=3
|
| 434 |
-
)
|
| 435 |
-
|
| 436 |
-
with gr.Row():
|
| 437 |
-
cancel_campaign_btn = gr.Button("Cancel")
|
| 438 |
-
save_campaign_btn = gr.Button("Create & Launch Campaign", variant="primary")
|
| 439 |
-
|
| 440 |
-
campaign_result = gr.Textbox(label="Result", interactive=False)
|
| 441 |
-
|
| 442 |
-
# Contacts View
|
| 443 |
-
with gr.Column(visible=False) as contacts_view:
|
| 444 |
-
gr.Markdown("### 👥 Contacts")
|
| 445 |
-
|
| 446 |
-
with gr.Row():
|
| 447 |
-
contact_search = gr.Textbox(
|
| 448 |
-
placeholder="Search contacts...",
|
| 449 |
-
show_label=False,
|
| 450 |
-
scale=3
|
| 451 |
-
)
|
| 452 |
-
contact_status_filter = gr.Dropdown(
|
| 453 |
-
choices=["all", "new", "contacted", "responded", "meeting_scheduled"],
|
| 454 |
-
value="all",
|
| 455 |
-
label="Filter by status",
|
| 456 |
-
scale=1
|
| 457 |
-
)
|
| 458 |
-
|
| 459 |
-
contacts_list = gr.HTML()
|
| 460 |
-
|
| 461 |
-
# Sequences View (Placeholder)
|
| 462 |
-
with gr.Column(visible=False) as sequences_view:
|
| 463 |
-
gr.Markdown("### 📧 Email Sequences")
|
| 464 |
-
gr.HTML(create_empty_state(
|
| 465 |
-
"📧",
|
| 466 |
-
"Sequences coming soon",
|
| 467 |
-
"Email sequence builder will be available in the next update"
|
| 468 |
-
))
|
| 469 |
-
|
| 470 |
-
# Analytics View (Placeholder)
|
| 471 |
-
with gr.Column(visible=False) as analytics_view:
|
| 472 |
-
gr.Markdown("### 📈 Analytics")
|
| 473 |
-
gr.HTML(create_empty_state(
|
| 474 |
-
"📊",
|
| 475 |
-
"Analytics dashboard coming soon",
|
| 476 |
-
"Comprehensive analytics and reporting will be available soon"
|
| 477 |
-
))
|
| 478 |
-
|
| 479 |
-
# Navigation logic
|
| 480 |
-
def switch_view(view_name):
|
| 481 |
-
return {
|
| 482 |
-
dashboard_view: gr.update(visible=view_name == "dashboard"),
|
| 483 |
-
campaigns_view: gr.update(visible=view_name == "campaigns"),
|
| 484 |
-
contacts_view: gr.update(visible=view_name == "contacts"),
|
| 485 |
-
sequences_view: gr.update(visible=view_name == "sequences"),
|
| 486 |
-
analytics_view: gr.update(visible=view_name == "analytics"),
|
| 487 |
-
}
|
| 488 |
-
|
| 489 |
-
nav_dashboard.click(lambda: switch_view("dashboard"), outputs=[dashboard_view, campaigns_view, contacts_view, sequences_view, analytics_view])
|
| 490 |
-
nav_campaigns.click(lambda: switch_view("campaigns"), outputs=[dashboard_view, campaigns_view, contacts_view, sequences_view, analytics_view])
|
| 491 |
-
nav_contacts.click(lambda: switch_view("contacts"), outputs=[dashboard_view, campaigns_view, contacts_view, sequences_view, analytics_view])
|
| 492 |
-
nav_sequences.click(lambda: switch_view("sequences"), outputs=[dashboard_view, campaigns_view, contacts_view, sequences_view, analytics_view])
|
| 493 |
-
nav_analytics.click(lambda: switch_view("analytics"), outputs=[dashboard_view, campaigns_view, contacts_view, sequences_view, analytics_view])
|
| 494 |
-
|
| 495 |
-
# Campaign creation logic
|
| 496 |
-
create_campaign_btn.click(
|
| 497 |
-
lambda: gr.update(visible=True),
|
| 498 |
-
outputs=[campaign_form]
|
| 499 |
-
)
|
| 500 |
-
|
| 501 |
-
cancel_campaign_btn.click(
|
| 502 |
-
lambda: gr.update(visible=False),
|
| 503 |
-
outputs=[campaign_form]
|
| 504 |
-
)
|
| 505 |
-
|
| 506 |
-
save_campaign_btn.click(
|
| 507 |
-
create_campaign_from_ui,
|
| 508 |
-
inputs=[campaign_name, campaign_description, campaign_companies],
|
| 509 |
-
outputs=[campaign_result]
|
| 510 |
-
)
|
| 511 |
-
|
| 512 |
-
# Contacts filtering
|
| 513 |
-
contact_search.change(
|
| 514 |
-
show_contacts,
|
| 515 |
-
inputs=[contact_search, contact_status_filter],
|
| 516 |
-
outputs=[contacts_list]
|
| 517 |
-
)
|
| 518 |
-
|
| 519 |
-
contact_status_filter.change(
|
| 520 |
-
show_contacts,
|
| 521 |
-
inputs=[contact_search, contact_status_filter],
|
| 522 |
-
outputs=[contacts_list]
|
| 523 |
-
)
|
| 524 |
-
|
| 525 |
-
# Initialize on load
|
| 526 |
-
demo.load(fn=initialize_system, outputs=[])
|
| 527 |
-
demo.load(fn=show_dashboard, outputs=[dashboard_content])
|
| 528 |
-
demo.load(fn=show_campaigns, outputs=[campaigns_list])
|
| 529 |
-
demo.load(fn=show_contacts, outputs=[contacts_list])
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
if __name__ == "__main__":
|
| 533 |
-
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,230 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Database Initialization Script
|
| 3 |
-
Creates all tables and loads sample data for the CX platform
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from sqlalchemy import create_engine, text
|
| 9 |
-
from models.cx_models import Base
|
| 10 |
-
from database.manager import get_db_manager
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
def init_database(db_path: str = None):
|
| 14 |
-
"""
|
| 15 |
-
Initialize database with all tables
|
| 16 |
-
|
| 17 |
-
Args:
|
| 18 |
-
db_path: Optional path to database file
|
| 19 |
-
"""
|
| 20 |
-
if db_path is None:
|
| 21 |
-
db_path = os.getenv('DATABASE_PATH', './data/cx_agent.db')
|
| 22 |
-
|
| 23 |
-
# Ensure data directory exists
|
| 24 |
-
data_dir = Path(db_path).parent
|
| 25 |
-
data_dir.mkdir(parents=True, exist_ok=True)
|
| 26 |
-
|
| 27 |
-
print(f"Initializing database at: {db_path}")
|
| 28 |
-
|
| 29 |
-
# Create engine
|
| 30 |
-
engine = create_engine(f'sqlite:///{db_path}')
|
| 31 |
-
|
| 32 |
-
# Create all tables
|
| 33 |
-
Base.metadata.create_all(engine)
|
| 34 |
-
|
| 35 |
-
print("✅ All tables created successfully!")
|
| 36 |
-
|
| 37 |
-
# Load sample data
|
| 38 |
-
load_sample_data(db_path)
|
| 39 |
-
|
| 40 |
-
print("✅ Database initialization complete!")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def load_sample_data(db_path: str):
|
| 44 |
-
"""Load sample data for testing"""
|
| 45 |
-
db = get_db_manager(db_path)
|
| 46 |
-
|
| 47 |
-
with db.get_session() as session:
|
| 48 |
-
from models.cx_models import (
|
| 49 |
-
CXCustomer, CXKBCategory, CXKBArticle, CXCannedResponse
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
# Check if data already exists
|
| 53 |
-
existing_customers = session.query(CXCustomer).count()
|
| 54 |
-
if existing_customers > 0:
|
| 55 |
-
print("Sample data already exists, skipping...")
|
| 56 |
-
return
|
| 57 |
-
|
| 58 |
-
# Create sample customer
|
| 59 |
-
customer = CXCustomer(
|
| 60 |
-
email="[email protected]",
|
| 61 |
-
first_name="Demo",
|
| 62 |
-
last_name="Customer",
|
| 63 |
-
company="Demo Company",
|
| 64 |
-
segment="standard",
|
| 65 |
-
lifecycle_stage="active"
|
| 66 |
-
)
|
| 67 |
-
session.add(customer)
|
| 68 |
-
|
| 69 |
-
# Create KB categories
|
| 70 |
-
cat_general = CXKBCategory(name="General", description="General information", icon="📘")
|
| 71 |
-
cat_technical = CXKBCategory(name="Technical", description="Technical guides", icon="⚙️")
|
| 72 |
-
cat_billing = CXKBCategory(name="Billing", description="Billing and payments", icon="💳")
|
| 73 |
-
|
| 74 |
-
session.add_all([cat_general, cat_technical, cat_billing])
|
| 75 |
-
session.flush()
|
| 76 |
-
|
| 77 |
-
# Create sample KB articles
|
| 78 |
-
articles = [
|
| 79 |
-
CXKBArticle(
|
| 80 |
-
category_id=cat_general.id,
|
| 81 |
-
title="Getting Started Guide",
|
| 82 |
-
summary="Learn how to get started with our platform",
|
| 83 |
-
content="""# Getting Started
|
| 84 |
-
|
| 85 |
-
Welcome to our platform! This guide will help you get up and running quickly.
|
| 86 |
-
|
| 87 |
-
## Step 1: Create Your Account
|
| 88 |
-
|
| 89 |
-
Visit our signup page and create an account using your email address.
|
| 90 |
-
|
| 91 |
-
## Step 2: Set Up Your Profile
|
| 92 |
-
|
| 93 |
-
Complete your profile with your company information and preferences.
|
| 94 |
-
|
| 95 |
-
## Step 3: Explore Features
|
| 96 |
-
|
| 97 |
-
Take a tour of our main features:
|
| 98 |
-
- Dashboard for overview
|
| 99 |
-
- Projects for collaboration
|
| 100 |
-
- Reports for insights
|
| 101 |
-
|
| 102 |
-
## Need Help?
|
| 103 |
-
|
| 104 |
-
Contact our support team at [email protected] or use the live chat feature.
|
| 105 |
-
""",
|
| 106 |
-
slug="getting-started-guide",
|
| 107 |
-
status="published",
|
| 108 |
-
visibility="public",
|
| 109 |
-
author="Support Team"
|
| 110 |
-
),
|
| 111 |
-
CXKBArticle(
|
| 112 |
-
category_id=cat_technical.id,
|
| 113 |
-
title="How to Reset Your Password",
|
| 114 |
-
summary="Step-by-step guide to reset your password",
|
| 115 |
-
content="""# Password Reset Guide
|
| 116 |
-
|
| 117 |
-
Forgot your password? No problem! Follow these steps:
|
| 118 |
-
|
| 119 |
-
## Steps:
|
| 120 |
-
|
| 121 |
-
1. Go to the login page
|
| 122 |
-
2. Click "Forgot Password"
|
| 123 |
-
3. Enter your email address
|
| 124 |
-
4. Check your email for reset link
|
| 125 |
-
5. Click the link and create a new password
|
| 126 |
-
|
| 127 |
-
## Password Requirements:
|
| 128 |
-
|
| 129 |
-
- At least 8 characters
|
| 130 |
-
- Include uppercase and lowercase letters
|
| 131 |
-
- Include at least one number
|
| 132 |
-
- Include a special character
|
| 133 |
-
|
| 134 |
-
## Still Having Issues?
|
| 135 |
-
|
| 136 |
-
Contact support if you don't receive the reset email within 5 minutes.
|
| 137 |
-
""",
|
| 138 |
-
slug="reset-password",
|
| 139 |
-
status="published",
|
| 140 |
-
visibility="public",
|
| 141 |
-
author="Support Team"
|
| 142 |
-
),
|
| 143 |
-
CXKBArticle(
|
| 144 |
-
category_id=cat_billing.id,
|
| 145 |
-
title="Understanding Your Invoice",
|
| 146 |
-
summary="Explanation of invoice charges and billing cycles",
|
| 147 |
-
content="""# Invoice Guide
|
| 148 |
-
|
| 149 |
-
This guide explains the charges on your invoice.
|
| 150 |
-
|
| 151 |
-
## Billing Cycle
|
| 152 |
-
|
| 153 |
-
Your billing cycle runs from the 1st to the last day of each month.
|
| 154 |
-
|
| 155 |
-
## Common Charges:
|
| 156 |
-
|
| 157 |
-
### Base Subscription
|
| 158 |
-
- Your plan tier (Basic, Pro, Enterprise)
|
| 159 |
-
- Billed monthly or annually
|
| 160 |
-
|
| 161 |
-
### Usage Charges
|
| 162 |
-
- Additional users beyond plan limit
|
| 163 |
-
- Extra storage
|
| 164 |
-
- API calls above quota
|
| 165 |
-
|
| 166 |
-
### One-Time Charges
|
| 167 |
-
- Setup fees
|
| 168 |
-
- Custom integrations
|
| 169 |
-
- Professional services
|
| 170 |
-
|
| 171 |
-
## Payment Methods
|
| 172 |
-
|
| 173 |
-
We accept:
|
| 174 |
-
- Credit cards (Visa, MasterCard, Amex)
|
| 175 |
-
- ACH/Bank transfer (Enterprise only)
|
| 176 |
-
- PayPal
|
| 177 |
-
|
| 178 |
-
## Questions?
|
| 179 |
-
|
| 180 |
-
Email [email protected] for invoice questions.
|
| 181 |
-
""",
|
| 182 |
-
slug="invoice-guide",
|
| 183 |
-
status="published",
|
| 184 |
-
visibility="public",
|
| 185 |
-
author="Billing Team"
|
| 186 |
-
)
|
| 187 |
-
]
|
| 188 |
-
|
| 189 |
-
session.add_all(articles)
|
| 190 |
-
|
| 191 |
-
# Create canned responses
|
| 192 |
-
responses = [
|
| 193 |
-
CXCannedResponse(
|
| 194 |
-
name="Greeting",
|
| 195 |
-
shortcut="/hello",
|
| 196 |
-
category="general",
|
| 197 |
-
content="Hello! Thank you for contacting us. How can I help you today?"
|
| 198 |
-
),
|
| 199 |
-
CXCannedResponse(
|
| 200 |
-
name="Password Reset",
|
| 201 |
-
shortcut="/resetpw",
|
| 202 |
-
category="technical",
|
| 203 |
-
subject="Password Reset Instructions",
|
| 204 |
-
content="""I can help you reset your password. Please follow these steps:
|
| 205 |
-
|
| 206 |
-
1. Go to the login page
|
| 207 |
-
2. Click "Forgot Password"
|
| 208 |
-
3. Enter your email address
|
| 209 |
-
4. Check your email for a reset link
|
| 210 |
-
|
| 211 |
-
If you don't receive the email within 5 minutes, please let me know and I'll escalate this to our technical team.
|
| 212 |
-
"""
|
| 213 |
-
),
|
| 214 |
-
CXCannedResponse(
|
| 215 |
-
name="Escalation",
|
| 216 |
-
shortcut="/escalate",
|
| 217 |
-
category="general",
|
| 218 |
-
content="I'm escalating your case to our specialist team. They will contact you within 24 hours. Is there anything else I can help with in the meantime?"
|
| 219 |
-
)
|
| 220 |
-
]
|
| 221 |
-
|
| 222 |
-
session.add_all(responses)
|
| 223 |
-
|
| 224 |
-
session.commit()
|
| 225 |
-
|
| 226 |
-
print("✅ Sample data loaded successfully!")
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
if __name__ == "__main__":
|
| 230 |
-
init_database()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Database Manager for
|
| 3 |
Handles database initialization, migrations, and session management
|
| 4 |
"""
|
| 5 |
from sqlalchemy import create_engine, event
|
|
@@ -86,23 +86,10 @@ class DatabaseManager:
|
|
| 86 |
print(f"⚠️ Could not import enterprise models: {e}")
|
| 87 |
logger.warning(f"Could not import enterprise models: {e}")
|
| 88 |
|
| 89 |
-
# Import CX models and create tables
|
| 90 |
-
try:
|
| 91 |
-
print("🔨 Creating CX platform tables...")
|
| 92 |
-
from models.cx_models import Base as CXBase
|
| 93 |
-
CXBase.metadata.create_all(self.engine)
|
| 94 |
-
print("✅ CX platform tables created successfully!")
|
| 95 |
-
logger.info("CX platform tables created")
|
| 96 |
-
except Exception as e:
|
| 97 |
-
print(f"❌ Failed to create CX tables: {e}")
|
| 98 |
-
logger.error(f"Failed to create CX tables: {e}")
|
| 99 |
-
raise
|
| 100 |
-
|
| 101 |
logger.info(f"Database initialized at {self.db_path}")
|
| 102 |
|
| 103 |
# Initialize with default data
|
| 104 |
self._initialize_default_data()
|
| 105 |
-
self._initialize_cx_sample_data()
|
| 106 |
|
| 107 |
return True
|
| 108 |
|
|
@@ -260,96 +247,6 @@ Best regards,
|
|
| 260 |
session.rollback()
|
| 261 |
session.close()
|
| 262 |
|
| 263 |
-
def _initialize_cx_sample_data(self):
|
| 264 |
-
"""Initialize sample data for CX platform tables"""
|
| 265 |
-
try:
|
| 266 |
-
from models.cx_models import CXCustomer, CXKBCategory, CXKBArticle, CXCannedResponse
|
| 267 |
-
from datetime import datetime
|
| 268 |
-
|
| 269 |
-
session = self.Session()
|
| 270 |
-
|
| 271 |
-
# Check if already initialized
|
| 272 |
-
existing_customers = session.query(CXCustomer).first()
|
| 273 |
-
if existing_customers:
|
| 274 |
-
session.close()
|
| 275 |
-
logger.info("CX sample data already exists, skipping...")
|
| 276 |
-
return
|
| 277 |
-
|
| 278 |
-
# Create sample customer
|
| 279 |
-
customer = CXCustomer(
|
| 280 |
-
email="[email protected]",
|
| 281 |
-
first_name="Demo",
|
| 282 |
-
last_name="Customer",
|
| 283 |
-
company="Demo Company",
|
| 284 |
-
segment="standard",
|
| 285 |
-
lifecycle_stage="active",
|
| 286 |
-
first_interaction_at=datetime.utcnow()
|
| 287 |
-
)
|
| 288 |
-
session.add(customer)
|
| 289 |
-
|
| 290 |
-
# Create KB categories
|
| 291 |
-
cat_general = CXKBCategory(name="General", description="General information", icon="📘")
|
| 292 |
-
cat_technical = CXKBCategory(name="Technical", description="Technical guides", icon="⚙️")
|
| 293 |
-
cat_billing = CXKBCategory(name="Billing", description="Billing and payments", icon="💳")
|
| 294 |
-
|
| 295 |
-
session.add_all([cat_general, cat_technical, cat_billing])
|
| 296 |
-
session.flush()
|
| 297 |
-
|
| 298 |
-
# Create sample KB articles
|
| 299 |
-
articles = [
|
| 300 |
-
CXKBArticle(
|
| 301 |
-
category_id=cat_general.id,
|
| 302 |
-
title="Getting Started Guide",
|
| 303 |
-
summary="Learn how to get started with our platform",
|
| 304 |
-
content="# Getting Started\n\nWelcome to our platform!...",
|
| 305 |
-
slug="getting-started-guide",
|
| 306 |
-
status="published",
|
| 307 |
-
visibility="public",
|
| 308 |
-
author="Support Team",
|
| 309 |
-
published_at=datetime.utcnow()
|
| 310 |
-
),
|
| 311 |
-
CXKBArticle(
|
| 312 |
-
category_id=cat_technical.id,
|
| 313 |
-
title="How to Reset Your Password",
|
| 314 |
-
summary="Step-by-step guide to reset your password",
|
| 315 |
-
content="# Password Reset Guide\n\nForgot your password?...",
|
| 316 |
-
slug="reset-password",
|
| 317 |
-
status="published",
|
| 318 |
-
visibility="public",
|
| 319 |
-
author="Support Team",
|
| 320 |
-
published_at=datetime.utcnow()
|
| 321 |
-
)
|
| 322 |
-
]
|
| 323 |
-
session.add_all(articles)
|
| 324 |
-
|
| 325 |
-
# Create canned responses
|
| 326 |
-
responses = [
|
| 327 |
-
CXCannedResponse(
|
| 328 |
-
name="Greeting",
|
| 329 |
-
shortcut="/hello",
|
| 330 |
-
category="general",
|
| 331 |
-
content="Hello! Thank you for contacting us. How can I help you today?"
|
| 332 |
-
),
|
| 333 |
-
CXCannedResponse(
|
| 334 |
-
name="Password Reset",
|
| 335 |
-
shortcut="/resetpw",
|
| 336 |
-
category="technical",
|
| 337 |
-
subject="Password Reset Instructions",
|
| 338 |
-
content="I can help you reset your password. Please follow these steps..."
|
| 339 |
-
)
|
| 340 |
-
]
|
| 341 |
-
session.add_all(responses)
|
| 342 |
-
|
| 343 |
-
session.commit()
|
| 344 |
-
session.close()
|
| 345 |
-
|
| 346 |
-
logger.info("CX sample data initialized successfully")
|
| 347 |
-
|
| 348 |
-
except Exception as e:
|
| 349 |
-
logger.error(f"Failed to initialize CX sample data: {str(e)}")
|
| 350 |
-
if session:
|
| 351 |
-
session.rollback()
|
| 352 |
-
session.close()
|
| 353 |
|
| 354 |
@contextmanager
|
| 355 |
def get_session(self):
|
|
|
|
| 1 |
"""
|
| 2 |
+
Database Manager for B2B Sales AI Agent
|
| 3 |
Handles database initialization, migrations, and session management
|
| 4 |
"""
|
| 5 |
from sqlalchemy import create_engine, event
|
|
|
|
| 86 |
print(f"⚠️ Could not import enterprise models: {e}")
|
| 87 |
logger.warning(f"Could not import enterprise models: {e}")
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
logger.info(f"Database initialized at {self.db_path}")
|
| 90 |
|
| 91 |
# Initialize with default data
|
| 92 |
self._initialize_default_data()
|
|
|
|
| 93 |
|
| 94 |
return True
|
| 95 |
|
|
|
|
| 247 |
session.rollback()
|
| 248 |
session.close()
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
@contextmanager
|
| 252 |
def get_session(self):
|
|
@@ -1,598 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
CX Platform - SQLAlchemy Models for Extended Schema
|
| 3 |
-
Maps to schema_extended.sql tables
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from sqlalchemy import Column, Integer, String, Float, Boolean, Text, ForeignKey, DateTime, Date, TIMESTAMP
|
| 7 |
-
from sqlalchemy.orm import relationship, declarative_base
|
| 8 |
-
from sqlalchemy.sql import func
|
| 9 |
-
from datetime import datetime
|
| 10 |
-
from typing import Optional, Dict, Any
|
| 11 |
-
import json
|
| 12 |
-
|
| 13 |
-
Base = declarative_base()
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
# =============================================================================
|
| 17 |
-
# CUSTOMERS
|
| 18 |
-
# =============================================================================
|
| 19 |
-
class CXCustomer(Base):
|
| 20 |
-
__tablename__ = 'cx_customers'
|
| 21 |
-
|
| 22 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 23 |
-
email = Column(String, unique=True, nullable=False)
|
| 24 |
-
first_name = Column(String)
|
| 25 |
-
last_name = Column(String)
|
| 26 |
-
company = Column(String)
|
| 27 |
-
phone = Column(String)
|
| 28 |
-
|
| 29 |
-
# Segmentation
|
| 30 |
-
segment = Column(String, default='standard') # vip, standard, at_risk, churned
|
| 31 |
-
lifecycle_stage = Column(String, default='active') # new, active, at_risk, churned
|
| 32 |
-
|
| 33 |
-
# Metrics
|
| 34 |
-
lifetime_value = Column(Float, default=0.0)
|
| 35 |
-
satisfaction_score = Column(Float, default=0.0) # CSAT average
|
| 36 |
-
nps_score = Column(Integer) # Net Promoter Score
|
| 37 |
-
sentiment = Column(String, default='neutral') # positive, neutral, negative
|
| 38 |
-
|
| 39 |
-
# Tracking
|
| 40 |
-
first_interaction_at = Column(TIMESTAMP)
|
| 41 |
-
last_interaction_at = Column(TIMESTAMP)
|
| 42 |
-
total_interactions = Column(Integer, default=0)
|
| 43 |
-
total_tickets = Column(Integer, default=0)
|
| 44 |
-
|
| 45 |
-
# Metadata
|
| 46 |
-
tags = Column(Text) # JSON array
|
| 47 |
-
custom_fields = Column(Text) # JSON object
|
| 48 |
-
notes = Column(Text)
|
| 49 |
-
|
| 50 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 51 |
-
updated_at = Column(TIMESTAMP, default=func.current_timestamp(), onupdate=func.current_timestamp())
|
| 52 |
-
|
| 53 |
-
# Relationships
|
| 54 |
-
tickets = relationship("CXTicket", back_populates="customer", cascade="all, delete-orphan")
|
| 55 |
-
chat_sessions = relationship("CXChatSession", back_populates="customer")
|
| 56 |
-
interactions = relationship("CXInteraction", back_populates="customer", cascade="all, delete-orphan")
|
| 57 |
-
|
| 58 |
-
@property
|
| 59 |
-
def full_name(self) -> str:
|
| 60 |
-
if self.first_name and self.last_name:
|
| 61 |
-
return f"{self.first_name} {self.last_name}"
|
| 62 |
-
return self.first_name or self.last_name or self.email
|
| 63 |
-
|
| 64 |
-
def get_tags(self) -> list:
|
| 65 |
-
return json.loads(self.tags) if self.tags else []
|
| 66 |
-
|
| 67 |
-
def set_tags(self, tags: list):
|
| 68 |
-
self.tags = json.dumps(tags)
|
| 69 |
-
|
| 70 |
-
def get_custom_fields(self) -> dict:
|
| 71 |
-
return json.loads(self.custom_fields) if self.custom_fields else {}
|
| 72 |
-
|
| 73 |
-
def set_custom_fields(self, fields: dict):
|
| 74 |
-
self.custom_fields = json.dumps(fields)
|
| 75 |
-
|
| 76 |
-
def to_dict(self) -> dict:
|
| 77 |
-
return {
|
| 78 |
-
'id': self.id,
|
| 79 |
-
'email': self.email,
|
| 80 |
-
'full_name': self.full_name,
|
| 81 |
-
'company': self.company,
|
| 82 |
-
'phone': self.phone,
|
| 83 |
-
'segment': self.segment,
|
| 84 |
-
'lifecycle_stage': self.lifecycle_stage,
|
| 85 |
-
'lifetime_value': self.lifetime_value,
|
| 86 |
-
'satisfaction_score': self.satisfaction_score,
|
| 87 |
-
'nps_score': self.nps_score,
|
| 88 |
-
'sentiment': self.sentiment,
|
| 89 |
-
'total_interactions': self.total_interactions,
|
| 90 |
-
'total_tickets': self.total_tickets,
|
| 91 |
-
'tags': self.get_tags(),
|
| 92 |
-
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
# =============================================================================
|
| 97 |
-
# TICKETS
|
| 98 |
-
# =============================================================================
|
| 99 |
-
class CXTicket(Base):
|
| 100 |
-
__tablename__ = 'cx_tickets'
|
| 101 |
-
|
| 102 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 103 |
-
customer_id = Column(Integer, ForeignKey('cx_customers.id', ondelete='CASCADE'), nullable=False)
|
| 104 |
-
|
| 105 |
-
# Core fields
|
| 106 |
-
subject = Column(String, nullable=False)
|
| 107 |
-
description = Column(Text)
|
| 108 |
-
status = Column(String, default='new') # new, open, pending, resolved, closed
|
| 109 |
-
priority = Column(String, default='medium') # low, medium, high, urgent
|
| 110 |
-
category = Column(String) # technical, billing, feature_request, etc.
|
| 111 |
-
|
| 112 |
-
# Assignment
|
| 113 |
-
assigned_to = Column(String) # agent name/id
|
| 114 |
-
assigned_team = Column(String)
|
| 115 |
-
|
| 116 |
-
# SLA
|
| 117 |
-
sla_due_at = Column(TIMESTAMP)
|
| 118 |
-
first_response_at = Column(TIMESTAMP)
|
| 119 |
-
resolved_at = Column(TIMESTAMP)
|
| 120 |
-
closed_at = Column(TIMESTAMP)
|
| 121 |
-
|
| 122 |
-
# Metrics
|
| 123 |
-
response_time_minutes = Column(Integer)
|
| 124 |
-
resolution_time_minutes = Column(Integer)
|
| 125 |
-
reopened_count = Column(Integer, default=0)
|
| 126 |
-
|
| 127 |
-
# AI fields
|
| 128 |
-
sentiment = Column(String) # detected from description
|
| 129 |
-
ai_suggested_category = Column(String)
|
| 130 |
-
ai_confidence = Column(Float)
|
| 131 |
-
auto_resolved = Column(Boolean, default=False)
|
| 132 |
-
|
| 133 |
-
# Metadata
|
| 134 |
-
source = Column(String, default='manual') # manual, email, chat, api, web_form
|
| 135 |
-
tags = Column(Text) # JSON array
|
| 136 |
-
custom_fields = Column(Text) # JSON
|
| 137 |
-
|
| 138 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 139 |
-
updated_at = Column(TIMESTAMP, default=func.current_timestamp(), onupdate=func.current_timestamp())
|
| 140 |
-
|
| 141 |
-
# Relationships
|
| 142 |
-
customer = relationship("CXCustomer", back_populates="tickets")
|
| 143 |
-
messages = relationship("CXTicketMessage", back_populates="ticket", cascade="all, delete-orphan")
|
| 144 |
-
attachments = relationship("CXTicketAttachment", back_populates="ticket", cascade="all, delete-orphan")
|
| 145 |
-
|
| 146 |
-
def get_tags(self) -> list:
|
| 147 |
-
return json.loads(self.tags) if self.tags else []
|
| 148 |
-
|
| 149 |
-
def set_tags(self, tags: list):
|
| 150 |
-
self.tags = json.dumps(tags)
|
| 151 |
-
|
| 152 |
-
def is_overdue(self) -> bool:
|
| 153 |
-
if not self.sla_due_at:
|
| 154 |
-
return False
|
| 155 |
-
return datetime.utcnow() > self.sla_due_at and self.status not in ['resolved', 'closed']
|
| 156 |
-
|
| 157 |
-
def to_dict(self) -> dict:
|
| 158 |
-
return {
|
| 159 |
-
'id': self.id,
|
| 160 |
-
'customer_id': self.customer_id,
|
| 161 |
-
'customer_name': self.customer.full_name if self.customer else None,
|
| 162 |
-
'subject': self.subject,
|
| 163 |
-
'description': self.description,
|
| 164 |
-
'status': self.status,
|
| 165 |
-
'priority': self.priority,
|
| 166 |
-
'category': self.category,
|
| 167 |
-
'assigned_to': self.assigned_to,
|
| 168 |
-
'sentiment': self.sentiment,
|
| 169 |
-
'is_overdue': self.is_overdue(),
|
| 170 |
-
'message_count': len(self.messages) if self.messages else 0,
|
| 171 |
-
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
class CXTicketMessage(Base):
|
| 176 |
-
__tablename__ = 'cx_ticket_messages'
|
| 177 |
-
|
| 178 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 179 |
-
ticket_id = Column(Integer, ForeignKey('cx_tickets.id', ondelete='CASCADE'), nullable=False)
|
| 180 |
-
|
| 181 |
-
# Sender
|
| 182 |
-
sender_type = Column(String, nullable=False) # customer, agent, system, ai_bot
|
| 183 |
-
sender_id = Column(String) # customer_id, agent_id, or 'system'
|
| 184 |
-
sender_name = Column(String)
|
| 185 |
-
|
| 186 |
-
# Message
|
| 187 |
-
message = Column(Text, nullable=False)
|
| 188 |
-
message_html = Column(Text)
|
| 189 |
-
is_internal = Column(Boolean, default=False) # internal note vs customer-visible
|
| 190 |
-
|
| 191 |
-
# AI fields
|
| 192 |
-
sentiment = Column(String)
|
| 193 |
-
intent = Column(String) # question, complaint, praise, feedback
|
| 194 |
-
|
| 195 |
-
# Metadata
|
| 196 |
-
meta_data = Column(Text) # JSON
|
| 197 |
-
|
| 198 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 199 |
-
|
| 200 |
-
# Relationships
|
| 201 |
-
ticket = relationship("CXTicket", back_populates="messages")
|
| 202 |
-
|
| 203 |
-
def to_dict(self) -> dict:
|
| 204 |
-
return {
|
| 205 |
-
'id': self.id,
|
| 206 |
-
'ticket_id': self.ticket_id,
|
| 207 |
-
'sender_type': self.sender_type,
|
| 208 |
-
'sender_name': self.sender_name,
|
| 209 |
-
'message': self.message,
|
| 210 |
-
'is_internal': self.is_internal,
|
| 211 |
-
'sentiment': self.sentiment,
|
| 212 |
-
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
class CXTicketAttachment(Base):
|
| 217 |
-
__tablename__ = 'cx_ticket_attachments'
|
| 218 |
-
|
| 219 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 220 |
-
ticket_id = Column(Integer, ForeignKey('cx_tickets.id', ondelete='CASCADE'), nullable=False)
|
| 221 |
-
message_id = Column(Integer, ForeignKey('cx_ticket_messages.id', ondelete='SET NULL'))
|
| 222 |
-
|
| 223 |
-
filename = Column(String, nullable=False)
|
| 224 |
-
file_path = Column(String, nullable=False)
|
| 225 |
-
file_size = Column(Integer)
|
| 226 |
-
mime_type = Column(String)
|
| 227 |
-
|
| 228 |
-
uploaded_by = Column(String)
|
| 229 |
-
uploaded_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 230 |
-
|
| 231 |
-
# Relationships
|
| 232 |
-
ticket = relationship("CXTicket", back_populates="attachments")
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
# =============================================================================
|
| 236 |
-
# KNOWLEDGE BASE
|
| 237 |
-
# =============================================================================
|
| 238 |
-
class CXKBCategory(Base):
|
| 239 |
-
__tablename__ = 'cx_kb_categories'
|
| 240 |
-
|
| 241 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 242 |
-
name = Column(String, nullable=False)
|
| 243 |
-
description = Column(Text)
|
| 244 |
-
parent_id = Column(Integer, ForeignKey('cx_kb_categories.id', ondelete='SET NULL'))
|
| 245 |
-
display_order = Column(Integer, default=0)
|
| 246 |
-
icon = Column(String)
|
| 247 |
-
|
| 248 |
-
is_active = Column(Boolean, default=True)
|
| 249 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 250 |
-
|
| 251 |
-
# Relationships
|
| 252 |
-
articles = relationship("CXKBArticle", back_populates="category")
|
| 253 |
-
|
| 254 |
-
def to_dict(self) -> dict:
|
| 255 |
-
return {
|
| 256 |
-
'id': self.id,
|
| 257 |
-
'name': self.name,
|
| 258 |
-
'description': self.description,
|
| 259 |
-
'parent_id': self.parent_id,
|
| 260 |
-
'article_count': len(self.articles) if self.articles else 0
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
class CXKBArticle(Base):
|
| 265 |
-
__tablename__ = 'cx_kb_articles'
|
| 266 |
-
|
| 267 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 268 |
-
category_id = Column(Integer, ForeignKey('cx_kb_categories.id', ondelete='SET NULL'))
|
| 269 |
-
|
| 270 |
-
# Content
|
| 271 |
-
title = Column(String, nullable=False)
|
| 272 |
-
summary = Column(Text)
|
| 273 |
-
content = Column(Text, nullable=False)
|
| 274 |
-
content_html = Column(Text)
|
| 275 |
-
|
| 276 |
-
# Status
|
| 277 |
-
status = Column(String, default='draft') # draft, published, archived
|
| 278 |
-
visibility = Column(String, default='public') # public, internal, private
|
| 279 |
-
|
| 280 |
-
# SEO
|
| 281 |
-
slug = Column(String, unique=True)
|
| 282 |
-
meta_description = Column(Text)
|
| 283 |
-
|
| 284 |
-
# Metrics
|
| 285 |
-
view_count = Column(Integer, default=0)
|
| 286 |
-
helpful_count = Column(Integer, default=0)
|
| 287 |
-
not_helpful_count = Column(Integer, default=0)
|
| 288 |
-
average_rating = Column(Float, default=0.0)
|
| 289 |
-
|
| 290 |
-
# AI fields
|
| 291 |
-
ai_generated = Column(Boolean, default=False)
|
| 292 |
-
ai_confidence = Column(Float)
|
| 293 |
-
keywords = Column(Text) # JSON array for semantic search
|
| 294 |
-
|
| 295 |
-
# Versioning
|
| 296 |
-
version = Column(Integer, default=1)
|
| 297 |
-
|
| 298 |
-
# Metadata
|
| 299 |
-
tags = Column(Text) # JSON array
|
| 300 |
-
related_articles = Column(Text) # JSON array of article IDs
|
| 301 |
-
|
| 302 |
-
# Authoring
|
| 303 |
-
author = Column(String)
|
| 304 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 305 |
-
updated_at = Column(TIMESTAMP, default=func.current_timestamp(), onupdate=func.current_timestamp())
|
| 306 |
-
published_at = Column(TIMESTAMP)
|
| 307 |
-
|
| 308 |
-
# Relationships
|
| 309 |
-
category = relationship("CXKBCategory", back_populates="articles")
|
| 310 |
-
versions = relationship("CXKBArticleVersion", back_populates="article", cascade="all, delete-orphan")
|
| 311 |
-
|
| 312 |
-
def get_keywords(self) -> list:
|
| 313 |
-
return json.loads(self.keywords) if self.keywords else []
|
| 314 |
-
|
| 315 |
-
def helpfulness_score(self) -> float:
|
| 316 |
-
total = self.helpful_count + self.not_helpful_count
|
| 317 |
-
if total == 0:
|
| 318 |
-
return 0.0
|
| 319 |
-
return self.helpful_count / total
|
| 320 |
-
|
| 321 |
-
def to_dict(self) -> dict:
|
| 322 |
-
return {
|
| 323 |
-
'id': self.id,
|
| 324 |
-
'title': self.title,
|
| 325 |
-
'summary': self.summary,
|
| 326 |
-
'status': self.status,
|
| 327 |
-
'view_count': self.view_count,
|
| 328 |
-
'helpful_count': self.helpful_count,
|
| 329 |
-
'helpfulness': self.helpfulness_score(),
|
| 330 |
-
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 331 |
-
}
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
class CXKBArticleVersion(Base):
|
| 335 |
-
__tablename__ = 'cx_kb_article_versions'
|
| 336 |
-
|
| 337 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 338 |
-
article_id = Column(Integer, ForeignKey('cx_kb_articles.id', ondelete='CASCADE'), nullable=False)
|
| 339 |
-
|
| 340 |
-
version = Column(Integer, nullable=False)
|
| 341 |
-
title = Column(String, nullable=False)
|
| 342 |
-
content = Column(Text, nullable=False)
|
| 343 |
-
|
| 344 |
-
changed_by = Column(String)
|
| 345 |
-
change_note = Column(Text)
|
| 346 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 347 |
-
|
| 348 |
-
# Relationships
|
| 349 |
-
article = relationship("CXKBArticle", back_populates="versions")
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
# =============================================================================
|
| 353 |
-
# LIVE CHAT
|
| 354 |
-
# =============================================================================
|
| 355 |
-
class CXChatSession(Base):
|
| 356 |
-
__tablename__ = 'cx_chat_sessions'
|
| 357 |
-
|
| 358 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 359 |
-
customer_id = Column(Integer, ForeignKey('cx_customers.id', ondelete='SET NULL'))
|
| 360 |
-
|
| 361 |
-
# Session info
|
| 362 |
-
session_id = Column(String, unique=True, nullable=False)
|
| 363 |
-
status = Column(String, default='active') # active, waiting, assigned, closed
|
| 364 |
-
|
| 365 |
-
# Routing
|
| 366 |
-
assigned_to = Column(String) # agent name/id
|
| 367 |
-
assigned_at = Column(TIMESTAMP)
|
| 368 |
-
|
| 369 |
-
# AI bot
|
| 370 |
-
bot_active = Column(Boolean, default=True)
|
| 371 |
-
bot_handed_off = Column(Boolean, default=False)
|
| 372 |
-
bot_handoff_reason = Column(String)
|
| 373 |
-
|
| 374 |
-
# Metrics
|
| 375 |
-
wait_time_seconds = Column(Integer, default=0)
|
| 376 |
-
response_time_seconds = Column(Integer, default=0)
|
| 377 |
-
message_count = Column(Integer, default=0)
|
| 378 |
-
|
| 379 |
-
# Metadata
|
| 380 |
-
page_url = Column(String)
|
| 381 |
-
referrer = Column(String)
|
| 382 |
-
user_agent = Column(String)
|
| 383 |
-
ip_address = Column(String)
|
| 384 |
-
|
| 385 |
-
# Satisfaction
|
| 386 |
-
rated = Column(Boolean, default=False)
|
| 387 |
-
rating = Column(Integer)
|
| 388 |
-
feedback = Column(Text)
|
| 389 |
-
|
| 390 |
-
started_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 391 |
-
ended_at = Column(TIMESTAMP)
|
| 392 |
-
|
| 393 |
-
# Relationships
|
| 394 |
-
customer = relationship("CXCustomer", back_populates="chat_sessions")
|
| 395 |
-
messages = relationship("CXChatMessage", back_populates="session", cascade="all, delete-orphan")
|
| 396 |
-
|
| 397 |
-
def to_dict(self) -> dict:
|
| 398 |
-
return {
|
| 399 |
-
'id': self.id,
|
| 400 |
-
'session_id': self.session_id,
|
| 401 |
-
'customer_name': self.customer.full_name if self.customer else 'Anonymous',
|
| 402 |
-
'status': self.status,
|
| 403 |
-
'assigned_to': self.assigned_to,
|
| 404 |
-
'bot_active': self.bot_active,
|
| 405 |
-
'message_count': self.message_count,
|
| 406 |
-
'rating': self.rating,
|
| 407 |
-
'started_at': self.started_at.isoformat() if self.started_at else None
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
class CXChatMessage(Base):
|
| 412 |
-
__tablename__ = 'cx_chat_messages'
|
| 413 |
-
|
| 414 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 415 |
-
session_id = Column(Integer, ForeignKey('cx_chat_sessions.id', ondelete='CASCADE'), nullable=False)
|
| 416 |
-
|
| 417 |
-
# Sender
|
| 418 |
-
sender_type = Column(String, nullable=False) # customer, agent, bot, system
|
| 419 |
-
sender_id = Column(String)
|
| 420 |
-
sender_name = Column(String)
|
| 421 |
-
|
| 422 |
-
# Message
|
| 423 |
-
message = Column(Text, nullable=False)
|
| 424 |
-
message_type = Column(String, default='text') # text, image, file, system_message
|
| 425 |
-
|
| 426 |
-
# AI fields
|
| 427 |
-
is_bot_response = Column(Boolean, default=False)
|
| 428 |
-
bot_confidence = Column(Float)
|
| 429 |
-
intent = Column(String)
|
| 430 |
-
|
| 431 |
-
# Status
|
| 432 |
-
is_read = Column(Boolean, default=False)
|
| 433 |
-
read_at = Column(TIMESTAMP)
|
| 434 |
-
|
| 435 |
-
# Metadata
|
| 436 |
-
meta_data = Column(Text) # JSON
|
| 437 |
-
|
| 438 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 439 |
-
|
| 440 |
-
# Relationships
|
| 441 |
-
session = relationship("CXChatSession", back_populates="messages")
|
| 442 |
-
|
| 443 |
-
def to_dict(self) -> dict:
|
| 444 |
-
return {
|
| 445 |
-
'id': self.id,
|
| 446 |
-
'sender_type': self.sender_type,
|
| 447 |
-
'sender_name': self.sender_name,
|
| 448 |
-
'message': self.message,
|
| 449 |
-
'is_bot_response': self.is_bot_response,
|
| 450 |
-
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 451 |
-
}
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
# =============================================================================
|
| 455 |
-
# AUTOMATION & ANALYTICS
|
| 456 |
-
# =============================================================================
|
| 457 |
-
class CXAutomationRule(Base):
|
| 458 |
-
__tablename__ = 'cx_automation_rules'
|
| 459 |
-
|
| 460 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 461 |
-
|
| 462 |
-
name = Column(String, nullable=False)
|
| 463 |
-
description = Column(Text)
|
| 464 |
-
is_active = Column(Boolean, default=True)
|
| 465 |
-
|
| 466 |
-
# Trigger
|
| 467 |
-
trigger_type = Column(String, nullable=False) # ticket_created, ticket_updated, time_based, etc.
|
| 468 |
-
trigger_conditions = Column(Text, nullable=False) # JSON
|
| 469 |
-
|
| 470 |
-
# Actions
|
| 471 |
-
actions = Column(Text, nullable=False) # JSON array of actions
|
| 472 |
-
|
| 473 |
-
# Execution
|
| 474 |
-
execution_count = Column(Integer, default=0)
|
| 475 |
-
last_executed_at = Column(TIMESTAMP)
|
| 476 |
-
|
| 477 |
-
# Priority
|
| 478 |
-
priority = Column(Integer, default=0)
|
| 479 |
-
|
| 480 |
-
created_by = Column(String)
|
| 481 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 482 |
-
updated_at = Column(TIMESTAMP, default=func.current_timestamp(), onupdate=func.current_timestamp())
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
class CXInteraction(Base):
|
| 486 |
-
__tablename__ = 'cx_interactions'
|
| 487 |
-
|
| 488 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 489 |
-
customer_id = Column(Integer, ForeignKey('cx_customers.id', ondelete='CASCADE'), nullable=False)
|
| 490 |
-
|
| 491 |
-
type = Column(String, nullable=False) # ticket, chat, email, call, meeting
|
| 492 |
-
channel = Column(String) # web, email, phone, chat, api
|
| 493 |
-
|
| 494 |
-
summary = Column(Text)
|
| 495 |
-
sentiment = Column(String)
|
| 496 |
-
intent = Column(String)
|
| 497 |
-
|
| 498 |
-
# References
|
| 499 |
-
reference_type = Column(String) # ticket, chat_session, email, etc.
|
| 500 |
-
reference_id = Column(Integer)
|
| 501 |
-
|
| 502 |
-
# Metrics
|
| 503 |
-
duration_seconds = Column(Integer)
|
| 504 |
-
satisfaction_rating = Column(Integer)
|
| 505 |
-
|
| 506 |
-
# Agent
|
| 507 |
-
handled_by = Column(String)
|
| 508 |
-
|
| 509 |
-
occurred_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 510 |
-
|
| 511 |
-
# Relationships
|
| 512 |
-
customer = relationship("CXCustomer", back_populates="interactions")
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
class CXAnalyticsDaily(Base):
|
| 516 |
-
__tablename__ = 'cx_analytics_daily'
|
| 517 |
-
|
| 518 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 519 |
-
date = Column(Date, nullable=False, unique=True)
|
| 520 |
-
|
| 521 |
-
# Ticket metrics
|
| 522 |
-
tickets_created = Column(Integer, default=0)
|
| 523 |
-
tickets_resolved = Column(Integer, default=0)
|
| 524 |
-
tickets_reopened = Column(Integer, default=0)
|
| 525 |
-
avg_resolution_time_minutes = Column(Float, default=0.0)
|
| 526 |
-
avg_first_response_minutes = Column(Float, default=0.0)
|
| 527 |
-
|
| 528 |
-
# Chat metrics
|
| 529 |
-
chats_started = Column(Integer, default=0)
|
| 530 |
-
chats_completed = Column(Integer, default=0)
|
| 531 |
-
avg_wait_time_seconds = Column(Float, default=0.0)
|
| 532 |
-
bot_resolution_rate = Column(Float, default=0.0)
|
| 533 |
-
|
| 534 |
-
# Satisfaction
|
| 535 |
-
avg_csat = Column(Float, default=0.0)
|
| 536 |
-
avg_nps = Column(Integer, default=0)
|
| 537 |
-
|
| 538 |
-
# KB metrics
|
| 539 |
-
kb_views = Column(Integer, default=0)
|
| 540 |
-
kb_helpful_votes = Column(Integer, default=0)
|
| 541 |
-
kb_searches = Column(Integer, default=0)
|
| 542 |
-
|
| 543 |
-
# Sentiment
|
| 544 |
-
positive_interactions = Column(Integer, default=0)
|
| 545 |
-
neutral_interactions = Column(Integer, default=0)
|
| 546 |
-
negative_interactions = Column(Integer, default=0)
|
| 547 |
-
|
| 548 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
class CXCannedResponse(Base):
|
| 552 |
-
__tablename__ = 'cx_canned_responses'
|
| 553 |
-
|
| 554 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 555 |
-
|
| 556 |
-
name = Column(String, nullable=False)
|
| 557 |
-
shortcut = Column(String, unique=True) # e.g., "/greeting"
|
| 558 |
-
category = Column(String)
|
| 559 |
-
|
| 560 |
-
subject = Column(String)
|
| 561 |
-
content = Column(Text, nullable=False)
|
| 562 |
-
|
| 563 |
-
# Usage
|
| 564 |
-
use_count = Column(Integer, default=0)
|
| 565 |
-
last_used_at = Column(TIMESTAMP)
|
| 566 |
-
|
| 567 |
-
is_active = Column(Boolean, default=True)
|
| 568 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
| 569 |
-
updated_at = Column(TIMESTAMP, default=func.current_timestamp(), onupdate=func.current_timestamp())
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
class CXAgentStats(Base):
|
| 573 |
-
__tablename__ = 'cx_agent_stats'
|
| 574 |
-
|
| 575 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 576 |
-
agent_id = Column(String, nullable=False)
|
| 577 |
-
agent_name = Column(String, nullable=False)
|
| 578 |
-
date = Column(Date, nullable=False)
|
| 579 |
-
|
| 580 |
-
# Tickets
|
| 581 |
-
tickets_handled = Column(Integer, default=0)
|
| 582 |
-
tickets_resolved = Column(Integer, default=0)
|
| 583 |
-
avg_resolution_time_minutes = Column(Float, default=0.0)
|
| 584 |
-
|
| 585 |
-
# Chats
|
| 586 |
-
chats_handled = Column(Integer, default=0)
|
| 587 |
-
avg_chat_duration_minutes = Column(Float, default=0.0)
|
| 588 |
-
|
| 589 |
-
# Quality
|
| 590 |
-
avg_csat = Column(Float, default=0.0)
|
| 591 |
-
positive_feedbacks = Column(Integer, default=0)
|
| 592 |
-
negative_feedbacks = Column(Integer, default=0)
|
| 593 |
-
|
| 594 |
-
# Efficiency
|
| 595 |
-
avg_response_time_minutes = Column(Float, default=0.0)
|
| 596 |
-
first_contact_resolutions = Column(Integer, default=0)
|
| 597 |
-
|
| 598 |
-
created_at = Column(TIMESTAMP, default=func.current_timestamp())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Analytics Module
|
| 3 |
-
Provides metrics, dashboards, and reporting for CX platform
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from .manager import AnalyticsManager
|
| 7 |
-
from .ui import render_analytics_ui
|
| 8 |
-
|
| 9 |
-
__all__ = ['AnalyticsManager', 'render_analytics_ui']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,346 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Analytics Manager - Aggregate metrics and generate reports
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
from typing import Dict, Any, List, Optional
|
| 6 |
-
from datetime import datetime, timedelta, date
|
| 7 |
-
from sqlalchemy import func
|
| 8 |
-
from database.manager import get_db_manager
|
| 9 |
-
from models.cx_models import (
|
| 10 |
-
CXTicket, CXChatSession, CXCustomer, CXKBArticle,
|
| 11 |
-
CXInteraction, CXAnalyticsDaily
|
| 12 |
-
)
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class AnalyticsManager:
|
| 16 |
-
"""Manages analytics and reporting across all modules"""
|
| 17 |
-
|
| 18 |
-
def __init__(self):
|
| 19 |
-
self.db = get_db_manager()
|
| 20 |
-
|
| 21 |
-
# =============================================================================
|
| 22 |
-
# OVERVIEW METRICS
|
| 23 |
-
# =============================================================================
|
| 24 |
-
|
| 25 |
-
def get_overview_metrics(self) -> Dict[str, Any]:
|
| 26 |
-
"""
|
| 27 |
-
Get high-level overview metrics for dashboard
|
| 28 |
-
|
| 29 |
-
Returns:
|
| 30 |
-
Dict with key metrics across all modules
|
| 31 |
-
"""
|
| 32 |
-
with self.db.get_session() as session:
|
| 33 |
-
# Customer metrics
|
| 34 |
-
total_customers = session.query(func.count(CXCustomer.id)).scalar()
|
| 35 |
-
active_customers = session.query(func.count(CXCustomer.id)).filter(
|
| 36 |
-
CXCustomer.lifecycle_stage == 'active'
|
| 37 |
-
).scalar()
|
| 38 |
-
avg_satisfaction = session.query(func.avg(CXCustomer.satisfaction_score)).scalar() or 0
|
| 39 |
-
|
| 40 |
-
# Ticket metrics
|
| 41 |
-
total_tickets = session.query(func.count(CXTicket.id)).scalar()
|
| 42 |
-
open_tickets = session.query(func.count(CXTicket.id)).filter(
|
| 43 |
-
CXTicket.status.in_(['new', 'open', 'pending'])
|
| 44 |
-
).scalar()
|
| 45 |
-
resolved_tickets = session.query(func.count(CXTicket.id)).filter(
|
| 46 |
-
CXTicket.status == 'resolved'
|
| 47 |
-
).scalar()
|
| 48 |
-
|
| 49 |
-
# Calculate avg resolution time
|
| 50 |
-
avg_resolution_time = session.query(func.avg(CXTicket.resolution_time_minutes)).filter(
|
| 51 |
-
CXTicket.resolution_time_minutes.isnot(None)
|
| 52 |
-
).scalar() or 0
|
| 53 |
-
|
| 54 |
-
# Chat metrics
|
| 55 |
-
total_chats = session.query(func.count(CXChatSession.id)).scalar()
|
| 56 |
-
active_chats = session.query(func.count(CXChatSession.id)).filter(
|
| 57 |
-
CXChatSession.status.in_(['active', 'waiting', 'assigned'])
|
| 58 |
-
).scalar()
|
| 59 |
-
avg_chat_rating = session.query(func.avg(CXChatSession.rating)).filter(
|
| 60 |
-
CXChatSession.rated == True
|
| 61 |
-
).scalar() or 0
|
| 62 |
-
|
| 63 |
-
# KB metrics
|
| 64 |
-
total_articles = session.query(func.count(CXKBArticle.id)).filter(
|
| 65 |
-
CXKBArticle.status == 'published'
|
| 66 |
-
).scalar()
|
| 67 |
-
total_kb_views = session.query(func.sum(CXKBArticle.view_count)).scalar() or 0
|
| 68 |
-
|
| 69 |
-
return {
|
| 70 |
-
'customers': {
|
| 71 |
-
'total': total_customers or 0,
|
| 72 |
-
'active': active_customers or 0,
|
| 73 |
-
'avg_satisfaction': round(avg_satisfaction, 2)
|
| 74 |
-
},
|
| 75 |
-
'tickets': {
|
| 76 |
-
'total': total_tickets or 0,
|
| 77 |
-
'open': open_tickets or 0,
|
| 78 |
-
'resolved': resolved_tickets or 0,
|
| 79 |
-
'avg_resolution_time_minutes': round(avg_resolution_time, 1),
|
| 80 |
-
'resolution_rate': round((resolved_tickets / total_tickets * 100) if total_tickets > 0 else 0, 1)
|
| 81 |
-
},
|
| 82 |
-
'chats': {
|
| 83 |
-
'total': total_chats or 0,
|
| 84 |
-
'active': active_chats or 0,
|
| 85 |
-
'avg_rating': round(avg_chat_rating, 2)
|
| 86 |
-
},
|
| 87 |
-
'knowledge_base': {
|
| 88 |
-
'total_articles': total_articles or 0,
|
| 89 |
-
'total_views': total_kb_views or 0
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
# =============================================================================
|
| 94 |
-
# TIME-SERIES METRICS
|
| 95 |
-
# =============================================================================
|
| 96 |
-
|
| 97 |
-
def get_metrics_by_date_range(
|
| 98 |
-
self,
|
| 99 |
-
start_date: datetime,
|
| 100 |
-
end_date: datetime
|
| 101 |
-
) -> Dict[str, Any]:
|
| 102 |
-
"""Get metrics for a specific date range"""
|
| 103 |
-
with self.db.get_session() as session:
|
| 104 |
-
# Tickets created in range
|
| 105 |
-
tickets_in_range = session.query(CXTicket).filter(
|
| 106 |
-
CXTicket.created_at >= start_date,
|
| 107 |
-
CXTicket.created_at <= end_date
|
| 108 |
-
).all()
|
| 109 |
-
|
| 110 |
-
# Chats in range
|
| 111 |
-
chats_in_range = session.query(CXChatSession).filter(
|
| 112 |
-
CXChatSession.started_at >= start_date,
|
| 113 |
-
CXChatSession.started_at <= end_date
|
| 114 |
-
).all()
|
| 115 |
-
|
| 116 |
-
# Calculate ticket metrics
|
| 117 |
-
tickets_created = len(tickets_in_range)
|
| 118 |
-
tickets_resolved = sum(1 for t in tickets_in_range if t.status == 'resolved')
|
| 119 |
-
resolution_times = [t.resolution_time_minutes for t in tickets_in_range if t.resolution_time_minutes]
|
| 120 |
-
|
| 121 |
-
# Calculate chat metrics
|
| 122 |
-
chats_started = len(chats_in_range)
|
| 123 |
-
chats_completed = sum(1 for c in chats_in_range if c.status == 'closed')
|
| 124 |
-
chat_ratings = [c.rating for c in chats_in_range if c.rated and c.rating]
|
| 125 |
-
|
| 126 |
-
return {
|
| 127 |
-
'period': {
|
| 128 |
-
'start': start_date.isoformat(),
|
| 129 |
-
'end': end_date.isoformat(),
|
| 130 |
-
'days': (end_date - start_date).days
|
| 131 |
-
},
|
| 132 |
-
'tickets': {
|
| 133 |
-
'created': tickets_created,
|
| 134 |
-
'resolved': tickets_resolved,
|
| 135 |
-
'avg_resolution_time': round(sum(resolution_times) / len(resolution_times), 1) if resolution_times else 0,
|
| 136 |
-
'resolution_rate': round((tickets_resolved / tickets_created * 100) if tickets_created > 0 else 0, 1)
|
| 137 |
-
},
|
| 138 |
-
'chats': {
|
| 139 |
-
'started': chats_started,
|
| 140 |
-
'completed': chats_completed,
|
| 141 |
-
'avg_rating': round(sum(chat_ratings) / len(chat_ratings), 2) if chat_ratings else 0
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
# =============================================================================
|
| 146 |
-
# CUSTOMER ANALYTICS
|
| 147 |
-
# =============================================================================
|
| 148 |
-
|
| 149 |
-
def get_customer_segmentation(self) -> Dict[str, int]:
|
| 150 |
-
"""Get customer count by segment"""
|
| 151 |
-
with self.db.get_session() as session:
|
| 152 |
-
results = session.query(
|
| 153 |
-
CXCustomer.segment,
|
| 154 |
-
func.count(CXCustomer.id)
|
| 155 |
-
).group_by(CXCustomer.segment).all()
|
| 156 |
-
|
| 157 |
-
return {segment: count for segment, count in results}
|
| 158 |
-
|
| 159 |
-
def get_customer_sentiment_distribution(self) -> Dict[str, int]:
|
| 160 |
-
"""Get customer count by sentiment"""
|
| 161 |
-
with self.db.get_session() as session:
|
| 162 |
-
results = session.query(
|
| 163 |
-
CXCustomer.sentiment,
|
| 164 |
-
func.count(CXCustomer.id)
|
| 165 |
-
).group_by(CXCustomer.sentiment).all()
|
| 166 |
-
|
| 167 |
-
return {sentiment: count for sentiment, count in results}
|
| 168 |
-
|
| 169 |
-
# =============================================================================
|
| 170 |
-
# TICKET ANALYTICS
|
| 171 |
-
# =============================================================================
|
| 172 |
-
|
| 173 |
-
def get_tickets_by_status(self) -> Dict[str, int]:
|
| 174 |
-
"""Get ticket count by status"""
|
| 175 |
-
with self.db.get_session() as session:
|
| 176 |
-
results = session.query(
|
| 177 |
-
CXTicket.status,
|
| 178 |
-
func.count(CXTicket.id)
|
| 179 |
-
).group_by(CXTicket.status).all()
|
| 180 |
-
|
| 181 |
-
return {status: count for status, count in results}
|
| 182 |
-
|
| 183 |
-
def get_tickets_by_priority(self) -> Dict[str, int]:
|
| 184 |
-
"""Get ticket count by priority"""
|
| 185 |
-
with self.db.get_session() as session:
|
| 186 |
-
results = session.query(
|
| 187 |
-
CXTicket.priority,
|
| 188 |
-
func.count(CXTicket.id)
|
| 189 |
-
).group_by(CXTicket.priority).all()
|
| 190 |
-
|
| 191 |
-
return {priority: count for priority, count in results}
|
| 192 |
-
|
| 193 |
-
def get_tickets_by_category(self) -> Dict[str, int]:
|
| 194 |
-
"""Get ticket count by category"""
|
| 195 |
-
with self.db.get_session() as session:
|
| 196 |
-
results = session.query(
|
| 197 |
-
CXTicket.category,
|
| 198 |
-
func.count(CXTicket.id)
|
| 199 |
-
).filter(
|
| 200 |
-
CXTicket.category.isnot(None)
|
| 201 |
-
).group_by(CXTicket.category).all()
|
| 202 |
-
|
| 203 |
-
return {category: count for category, count in results}
|
| 204 |
-
|
| 205 |
-
# =============================================================================
|
| 206 |
-
# SLA PERFORMANCE
|
| 207 |
-
# =============================================================================
|
| 208 |
-
|
| 209 |
-
def get_sla_performance(self) -> Dict[str, Any]:
|
| 210 |
-
"""Get SLA performance metrics"""
|
| 211 |
-
with self.db.get_session() as session:
|
| 212 |
-
now = datetime.utcnow()
|
| 213 |
-
|
| 214 |
-
# All open tickets
|
| 215 |
-
open_tickets = session.query(CXTicket).filter(
|
| 216 |
-
CXTicket.status.in_(['new', 'open', 'pending'])
|
| 217 |
-
).all()
|
| 218 |
-
|
| 219 |
-
breached = 0
|
| 220 |
-
at_risk = 0
|
| 221 |
-
on_track = 0
|
| 222 |
-
|
| 223 |
-
for ticket in open_tickets:
|
| 224 |
-
if ticket.sla_due_at:
|
| 225 |
-
if ticket.sla_due_at < now:
|
| 226 |
-
breached += 1
|
| 227 |
-
elif ticket.sla_due_at < now + timedelta(hours=2):
|
| 228 |
-
at_risk += 1
|
| 229 |
-
else:
|
| 230 |
-
on_track += 1
|
| 231 |
-
else:
|
| 232 |
-
on_track += 1
|
| 233 |
-
|
| 234 |
-
total = len(open_tickets)
|
| 235 |
-
|
| 236 |
-
return {
|
| 237 |
-
'breached': breached,
|
| 238 |
-
'at_risk': at_risk,
|
| 239 |
-
'on_track': on_track,
|
| 240 |
-
'total': total,
|
| 241 |
-
'sla_compliance_rate': round((on_track / total * 100) if total > 0 else 0, 1)
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
# =============================================================================
|
| 245 |
-
# TRENDING & FORECASTING
|
| 246 |
-
# =============================================================================
|
| 247 |
-
|
| 248 |
-
def get_weekly_trend(self, weeks: int = 4) -> List[Dict[str, Any]]:
|
| 249 |
-
"""Get weekly metrics trend"""
|
| 250 |
-
trends = []
|
| 251 |
-
end_date = datetime.utcnow()
|
| 252 |
-
|
| 253 |
-
for i in range(weeks):
|
| 254 |
-
week_start = end_date - timedelta(weeks=i+1)
|
| 255 |
-
week_end = end_date - timedelta(weeks=i)
|
| 256 |
-
|
| 257 |
-
metrics = self.get_metrics_by_date_range(week_start, week_end)
|
| 258 |
-
|
| 259 |
-
trends.append({
|
| 260 |
-
'week': f"Week {weeks - i}",
|
| 261 |
-
'start_date': week_start.strftime("%Y-%m-%d"),
|
| 262 |
-
'tickets_created': metrics['tickets']['created'],
|
| 263 |
-
'tickets_resolved': metrics['tickets']['resolved'],
|
| 264 |
-
'chats_started': metrics['chats']['started']
|
| 265 |
-
})
|
| 266 |
-
|
| 267 |
-
return list(reversed(trends))
|
| 268 |
-
|
| 269 |
-
# =============================================================================
|
| 270 |
-
# AGENT PERFORMANCE
|
| 271 |
-
# =============================================================================
|
| 272 |
-
|
| 273 |
-
def get_agent_performance_summary(self) -> List[Dict[str, Any]]:
|
| 274 |
-
"""Get agent performance summary (placeholder for future agent tracking)"""
|
| 275 |
-
# TODO: Implement when agent tracking is added
|
| 276 |
-
return [
|
| 277 |
-
{
|
| 278 |
-
'agent_name': 'Agent 1',
|
| 279 |
-
'tickets_handled': 0,
|
| 280 |
-
'tickets_resolved': 0,
|
| 281 |
-
'avg_resolution_time': 0,
|
| 282 |
-
'avg_csat': 0
|
| 283 |
-
}
|
| 284 |
-
]
|
| 285 |
-
|
| 286 |
-
# =============================================================================
|
| 287 |
-
# DAILY SNAPSHOTS
|
| 288 |
-
# =============================================================================
|
| 289 |
-
|
| 290 |
-
def create_daily_snapshot(self, target_date: Optional[date] = None) -> CXAnalyticsDaily:
|
| 291 |
-
"""
|
| 292 |
-
Create daily analytics snapshot
|
| 293 |
-
|
| 294 |
-
Args:
|
| 295 |
-
target_date: Date to create snapshot for (defaults to today)
|
| 296 |
-
"""
|
| 297 |
-
if target_date is None:
|
| 298 |
-
target_date = date.today()
|
| 299 |
-
|
| 300 |
-
with self.db.get_session() as session:
|
| 301 |
-
# Check if snapshot already exists
|
| 302 |
-
existing = session.query(CXAnalyticsDaily).filter_by(date=target_date).first()
|
| 303 |
-
if existing:
|
| 304 |
-
# Update existing
|
| 305 |
-
snapshot = existing
|
| 306 |
-
else:
|
| 307 |
-
# Create new
|
| 308 |
-
snapshot = CXAnalyticsDaily(date=target_date)
|
| 309 |
-
session.add(snapshot)
|
| 310 |
-
|
| 311 |
-
# Calculate ticket metrics for the day
|
| 312 |
-
day_start = datetime.combine(target_date, datetime.min.time())
|
| 313 |
-
day_end = datetime.combine(target_date, datetime.max.time())
|
| 314 |
-
|
| 315 |
-
tickets_created = session.query(func.count(CXTicket.id)).filter(
|
| 316 |
-
CXTicket.created_at >= day_start,
|
| 317 |
-
CXTicket.created_at <= day_end
|
| 318 |
-
).scalar() or 0
|
| 319 |
-
|
| 320 |
-
tickets_resolved = session.query(func.count(CXTicket.id)).filter(
|
| 321 |
-
CXTicket.resolved_at >= day_start,
|
| 322 |
-
CXTicket.resolved_at <= day_end
|
| 323 |
-
).scalar() or 0
|
| 324 |
-
|
| 325 |
-
# Chat metrics
|
| 326 |
-
chats_started = session.query(func.count(CXChatSession.id)).filter(
|
| 327 |
-
CXChatSession.started_at >= day_start,
|
| 328 |
-
CXChatSession.started_at <= day_end
|
| 329 |
-
).scalar() or 0
|
| 330 |
-
|
| 331 |
-
chats_completed = session.query(func.count(CXChatSession.id)).filter(
|
| 332 |
-
CXChatSession.ended_at >= day_start,
|
| 333 |
-
CXChatSession.ended_at <= day_end,
|
| 334 |
-
CXChatSession.status == 'closed'
|
| 335 |
-
).scalar() or 0
|
| 336 |
-
|
| 337 |
-
# Update snapshot
|
| 338 |
-
snapshot.tickets_created = tickets_created
|
| 339 |
-
snapshot.tickets_resolved = tickets_resolved
|
| 340 |
-
snapshot.chats_started = chats_started
|
| 341 |
-
snapshot.chats_completed = chats_completed
|
| 342 |
-
|
| 343 |
-
session.commit()
|
| 344 |
-
session.refresh(snapshot)
|
| 345 |
-
|
| 346 |
-
return snapshot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,248 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Analytics UI - Dashboard and reporting interface
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import gradio as gr
|
| 6 |
-
from datetime import datetime, timedelta
|
| 7 |
-
|
| 8 |
-
from .manager import AnalyticsManager
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class AnalyticsUI:
|
| 12 |
-
"""Gradio UI for analytics dashboard"""
|
| 13 |
-
|
| 14 |
-
def __init__(self):
|
| 15 |
-
self.manager = AnalyticsManager()
|
| 16 |
-
|
| 17 |
-
def render(self) -> gr.Blocks:
|
| 18 |
-
"""Render analytics dashboard"""
|
| 19 |
-
|
| 20 |
-
with gr.Blocks() as analytics_interface:
|
| 21 |
-
gr.Markdown("# 📊 Analytics Dashboard")
|
| 22 |
-
|
| 23 |
-
with gr.Tabs():
|
| 24 |
-
# TAB 1: Overview
|
| 25 |
-
with gr.Tab("📈 Overview"):
|
| 26 |
-
refresh_overview_btn = gr.Button("🔄 Refresh", variant="secondary")
|
| 27 |
-
|
| 28 |
-
gr.Markdown("## Key Metrics")
|
| 29 |
-
|
| 30 |
-
with gr.Row():
|
| 31 |
-
total_customers_display = gr.Number(label="Total Customers", value=0)
|
| 32 |
-
active_customers_display = gr.Number(label="Active Customers", value=0)
|
| 33 |
-
avg_satisfaction_display = gr.Number(label="Avg CSAT", value=0)
|
| 34 |
-
|
| 35 |
-
with gr.Row():
|
| 36 |
-
total_tickets_display = gr.Number(label="Total Tickets", value=0)
|
| 37 |
-
open_tickets_display = gr.Number(label="Open Tickets", value=0)
|
| 38 |
-
resolved_tickets_display = gr.Number(label="Resolved Tickets", value=0)
|
| 39 |
-
|
| 40 |
-
with gr.Row():
|
| 41 |
-
avg_resolution_time_display = gr.Number(label="Avg Resolution Time (min)", value=0)
|
| 42 |
-
resolution_rate_display = gr.Number(label="Resolution Rate (%)", value=0)
|
| 43 |
-
total_chats_display = gr.Number(label="Total Chats", value=0)
|
| 44 |
-
|
| 45 |
-
gr.Markdown("## Full Metrics")
|
| 46 |
-
overview_metrics_json = gr.JSON(label="All Metrics")
|
| 47 |
-
|
| 48 |
-
# TAB 2: Tickets
|
| 49 |
-
with gr.Tab("🎫 Ticket Analytics"):
|
| 50 |
-
refresh_tickets_btn = gr.Button("🔄 Refresh", variant="secondary")
|
| 51 |
-
|
| 52 |
-
gr.Markdown("### Ticket Distribution")
|
| 53 |
-
|
| 54 |
-
with gr.Row():
|
| 55 |
-
with gr.Column():
|
| 56 |
-
tickets_by_status = gr.JSON(label="By Status")
|
| 57 |
-
with gr.Column():
|
| 58 |
-
tickets_by_priority = gr.JSON(label="By Priority")
|
| 59 |
-
with gr.Column():
|
| 60 |
-
tickets_by_category = gr.JSON(label="By Category")
|
| 61 |
-
|
| 62 |
-
gr.Markdown("### SLA Performance")
|
| 63 |
-
sla_performance = gr.JSON(label="SLA Metrics")
|
| 64 |
-
|
| 65 |
-
# TAB 3: Customers
|
| 66 |
-
with gr.Tab("👥 Customer Analytics"):
|
| 67 |
-
refresh_customers_btn = gr.Button("🔄 Refresh", variant="secondary")
|
| 68 |
-
|
| 69 |
-
gr.Markdown("### Customer Segmentation")
|
| 70 |
-
|
| 71 |
-
with gr.Row():
|
| 72 |
-
customer_segmentation = gr.JSON(label="By Segment")
|
| 73 |
-
sentiment_distribution = gr.JSON(label="By Sentiment")
|
| 74 |
-
|
| 75 |
-
# TAB 4: Trends
|
| 76 |
-
with gr.Tab("📉 Trends"):
|
| 77 |
-
gr.Markdown("### Weekly Trends")
|
| 78 |
-
|
| 79 |
-
weeks_selector = gr.Slider(
|
| 80 |
-
minimum=2,
|
| 81 |
-
maximum=12,
|
| 82 |
-
value=4,
|
| 83 |
-
step=1,
|
| 84 |
-
label="Number of Weeks"
|
| 85 |
-
)
|
| 86 |
-
|
| 87 |
-
load_trends_btn = gr.Button("Load Trends", variant="primary")
|
| 88 |
-
|
| 89 |
-
trends_table = gr.Dataframe(
|
| 90 |
-
headers=["Week", "Start Date", "Tickets Created", "Tickets Resolved", "Chats Started"],
|
| 91 |
-
label="Weekly Performance",
|
| 92 |
-
interactive=False
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
# TAB 5: Reports
|
| 96 |
-
with gr.Tab("📄 Reports"):
|
| 97 |
-
gr.Markdown("### Generate Custom Report")
|
| 98 |
-
|
| 99 |
-
with gr.Row():
|
| 100 |
-
start_date_input = gr.Textbox(
|
| 101 |
-
label="Start Date (YYYY-MM-DD)",
|
| 102 |
-
value=(datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
| 103 |
-
)
|
| 104 |
-
end_date_input = gr.Textbox(
|
| 105 |
-
label="End Date (YYYY-MM-DD)",
|
| 106 |
-
value=datetime.now().strftime("%Y-%m-%d")
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
-
generate_report_btn = gr.Button("Generate Report", variant="primary")
|
| 110 |
-
|
| 111 |
-
report_output = gr.JSON(label="Report Data")
|
| 112 |
-
|
| 113 |
-
# =============================================================================
|
| 114 |
-
# EVENT HANDLERS
|
| 115 |
-
# =============================================================================
|
| 116 |
-
|
| 117 |
-
def load_overview():
|
| 118 |
-
"""Load overview metrics"""
|
| 119 |
-
try:
|
| 120 |
-
metrics = self.manager.get_overview_metrics()
|
| 121 |
-
|
| 122 |
-
return (
|
| 123 |
-
metrics['customers']['total'],
|
| 124 |
-
metrics['customers']['active'],
|
| 125 |
-
metrics['customers']['avg_satisfaction'],
|
| 126 |
-
metrics['tickets']['total'],
|
| 127 |
-
metrics['tickets']['open'],
|
| 128 |
-
metrics['tickets']['resolved'],
|
| 129 |
-
metrics['tickets']['avg_resolution_time_minutes'],
|
| 130 |
-
metrics['tickets']['resolution_rate'],
|
| 131 |
-
metrics['chats']['total'],
|
| 132 |
-
metrics
|
| 133 |
-
)
|
| 134 |
-
except Exception as e:
|
| 135 |
-
error_metrics = {"error": str(e)}
|
| 136 |
-
return (0, 0, 0, 0, 0, 0, 0, 0, 0, error_metrics)
|
| 137 |
-
|
| 138 |
-
def load_ticket_analytics():
|
| 139 |
-
"""Load ticket analytics"""
|
| 140 |
-
try:
|
| 141 |
-
by_status = self.manager.get_tickets_by_status()
|
| 142 |
-
by_priority = self.manager.get_tickets_by_priority()
|
| 143 |
-
by_category = self.manager.get_tickets_by_category()
|
| 144 |
-
sla = self.manager.get_sla_performance()
|
| 145 |
-
|
| 146 |
-
return by_status, by_priority, by_category, sla
|
| 147 |
-
|
| 148 |
-
except Exception as e:
|
| 149 |
-
error = {"error": str(e)}
|
| 150 |
-
return error, error, error, error
|
| 151 |
-
|
| 152 |
-
def load_customer_analytics():
|
| 153 |
-
"""Load customer analytics"""
|
| 154 |
-
try:
|
| 155 |
-
segmentation = self.manager.get_customer_segmentation()
|
| 156 |
-
sentiment = self.manager.get_customer_sentiment_distribution()
|
| 157 |
-
|
| 158 |
-
return segmentation, sentiment
|
| 159 |
-
|
| 160 |
-
except Exception as e:
|
| 161 |
-
error = {"error": str(e)}
|
| 162 |
-
return error, error
|
| 163 |
-
|
| 164 |
-
def load_weekly_trends(weeks):
|
| 165 |
-
"""Load weekly trends"""
|
| 166 |
-
try:
|
| 167 |
-
trends = self.manager.get_weekly_trend(weeks=int(weeks))
|
| 168 |
-
|
| 169 |
-
# Convert to table format
|
| 170 |
-
rows = []
|
| 171 |
-
for trend in trends:
|
| 172 |
-
rows.append([
|
| 173 |
-
trend['week'],
|
| 174 |
-
trend['start_date'],
|
| 175 |
-
trend['tickets_created'],
|
| 176 |
-
trend['tickets_resolved'],
|
| 177 |
-
trend['chats_started']
|
| 178 |
-
])
|
| 179 |
-
|
| 180 |
-
return rows
|
| 181 |
-
|
| 182 |
-
except Exception as e:
|
| 183 |
-
return [[f"Error: {str(e)}", "", "", "", ""]]
|
| 184 |
-
|
| 185 |
-
def generate_custom_report(start_date_str, end_date_str):
|
| 186 |
-
"""Generate custom date range report"""
|
| 187 |
-
try:
|
| 188 |
-
start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
|
| 189 |
-
end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
|
| 190 |
-
|
| 191 |
-
report = self.manager.get_metrics_by_date_range(start_date, end_date)
|
| 192 |
-
|
| 193 |
-
return report
|
| 194 |
-
|
| 195 |
-
except Exception as e:
|
| 196 |
-
return {"error": str(e)}
|
| 197 |
-
|
| 198 |
-
# Wire up events
|
| 199 |
-
refresh_overview_btn.click(
|
| 200 |
-
fn=load_overview,
|
| 201 |
-
outputs=[
|
| 202 |
-
total_customers_display, active_customers_display, avg_satisfaction_display,
|
| 203 |
-
total_tickets_display, open_tickets_display, resolved_tickets_display,
|
| 204 |
-
avg_resolution_time_display, resolution_rate_display, total_chats_display,
|
| 205 |
-
overview_metrics_json
|
| 206 |
-
]
|
| 207 |
-
)
|
| 208 |
-
|
| 209 |
-
refresh_tickets_btn.click(
|
| 210 |
-
fn=load_ticket_analytics,
|
| 211 |
-
outputs=[tickets_by_status, tickets_by_priority, tickets_by_category, sla_performance]
|
| 212 |
-
)
|
| 213 |
-
|
| 214 |
-
refresh_customers_btn.click(
|
| 215 |
-
fn=load_customer_analytics,
|
| 216 |
-
outputs=[customer_segmentation, sentiment_distribution]
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
load_trends_btn.click(
|
| 220 |
-
fn=load_weekly_trends,
|
| 221 |
-
inputs=[weeks_selector],
|
| 222 |
-
outputs=[trends_table]
|
| 223 |
-
)
|
| 224 |
-
|
| 225 |
-
generate_report_btn.click(
|
| 226 |
-
fn=generate_custom_report,
|
| 227 |
-
inputs=[start_date_input, end_date_input],
|
| 228 |
-
outputs=[report_output]
|
| 229 |
-
)
|
| 230 |
-
|
| 231 |
-
# Load initial data
|
| 232 |
-
analytics_interface.load(
|
| 233 |
-
fn=load_overview,
|
| 234 |
-
outputs=[
|
| 235 |
-
total_customers_display, active_customers_display, avg_satisfaction_display,
|
| 236 |
-
total_tickets_display, open_tickets_display, resolved_tickets_display,
|
| 237 |
-
avg_resolution_time_display, resolution_rate_display, total_chats_display,
|
| 238 |
-
overview_metrics_json
|
| 239 |
-
]
|
| 240 |
-
)
|
| 241 |
-
|
| 242 |
-
return analytics_interface
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
def render_analytics_ui() -> gr.Blocks:
|
| 246 |
-
"""Convenience function to render analytics UI"""
|
| 247 |
-
ui = AnalyticsUI()
|
| 248 |
-
return ui.render()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Live Chat Module
|
| 3 |
-
Handles real-time customer chat with AI bot and human handoff
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from .manager import ChatManager
|
| 7 |
-
from .bot import ChatBot
|
| 8 |
-
from .ui import render_chat_ui
|
| 9 |
-
|
| 10 |
-
__all__ = ['ChatManager', 'ChatBot', 'render_chat_ui']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,360 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
ChatBot - AI-powered chat bot with RAG
|
| 3 |
-
Uses knowledge base for context-aware responses
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import Optional, Dict, Any, Tuple
|
| 7 |
-
import os
|
| 8 |
-
import re
|
| 9 |
-
|
| 10 |
-
# Import knowledge base for RAG
|
| 11 |
-
from modules.knowledge.search import SemanticSearchEngine
|
| 12 |
-
from modules.knowledge.manager import KnowledgeBaseManager
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class ChatBot:
|
| 16 |
-
"""
|
| 17 |
-
AI-powered chatbot for customer support
|
| 18 |
-
|
| 19 |
-
Features:
|
| 20 |
-
- RAG-powered responses using knowledge base
|
| 21 |
-
- Intent detection
|
| 22 |
-
- Sentiment analysis
|
| 23 |
-
- Automatic escalation to human agent
|
| 24 |
-
- Context-aware conversations
|
| 25 |
-
"""
|
| 26 |
-
|
| 27 |
-
def __init__(self):
|
| 28 |
-
self.kb_search = SemanticSearchEngine()
|
| 29 |
-
self.kb_manager = KnowledgeBaseManager()
|
| 30 |
-
|
| 31 |
-
# Escalation triggers
|
| 32 |
-
self.escalation_keywords = [
|
| 33 |
-
'speak to human', 'talk to agent', 'real person',
|
| 34 |
-
'not helping', 'frustrated', 'angry', 'manager',
|
| 35 |
-
'cancel', 'refund', 'complaint'
|
| 36 |
-
]
|
| 37 |
-
|
| 38 |
-
# Greeting patterns
|
| 39 |
-
self.greeting_patterns = [
|
| 40 |
-
r'\b(hi|hello|hey|greetings)\b',
|
| 41 |
-
r'\bgood (morning|afternoon|evening)\b'
|
| 42 |
-
]
|
| 43 |
-
|
| 44 |
-
# =============================================================================
|
| 45 |
-
# MAIN RESPONSE GENERATION
|
| 46 |
-
# =============================================================================
|
| 47 |
-
|
| 48 |
-
def generate_response(
|
| 49 |
-
self,
|
| 50 |
-
message: str,
|
| 51 |
-
session_context: Optional[Dict[str, Any]] = None
|
| 52 |
-
) -> Tuple[str, Dict[str, Any]]:
|
| 53 |
-
"""
|
| 54 |
-
Generate AI response to customer message
|
| 55 |
-
|
| 56 |
-
Args:
|
| 57 |
-
message: Customer message
|
| 58 |
-
session_context: Optional session context (previous messages, customer info)
|
| 59 |
-
|
| 60 |
-
Returns:
|
| 61 |
-
Tuple of (response_text, metadata)
|
| 62 |
-
metadata includes: intent, sentiment, confidence, should_escalate, suggested_articles
|
| 63 |
-
"""
|
| 64 |
-
# Detect intent
|
| 65 |
-
intent = self.detect_intent(message)
|
| 66 |
-
|
| 67 |
-
# Detect sentiment
|
| 68 |
-
sentiment = self.detect_sentiment(message)
|
| 69 |
-
|
| 70 |
-
# Check if should escalate to human
|
| 71 |
-
should_escalate = self.should_escalate_to_human(message, sentiment, intent)
|
| 72 |
-
|
| 73 |
-
# Handle different intents
|
| 74 |
-
if intent == 'greeting':
|
| 75 |
-
response = self._handle_greeting(message)
|
| 76 |
-
confidence = 0.95
|
| 77 |
-
suggested_articles = []
|
| 78 |
-
|
| 79 |
-
elif intent == 'farewell':
|
| 80 |
-
response = self._handle_farewell(message)
|
| 81 |
-
confidence = 0.95
|
| 82 |
-
suggested_articles = []
|
| 83 |
-
|
| 84 |
-
elif should_escalate:
|
| 85 |
-
response = self._handle_escalation(message)
|
| 86 |
-
confidence = 0.90
|
| 87 |
-
suggested_articles = []
|
| 88 |
-
|
| 89 |
-
else:
|
| 90 |
-
# Use RAG to generate response
|
| 91 |
-
response, confidence, suggested_articles = self._generate_rag_response(message)
|
| 92 |
-
|
| 93 |
-
# Build metadata
|
| 94 |
-
metadata = {
|
| 95 |
-
'intent': intent,
|
| 96 |
-
'sentiment': sentiment,
|
| 97 |
-
'confidence': confidence,
|
| 98 |
-
'should_escalate': should_escalate,
|
| 99 |
-
'suggested_articles': [article.id for article, _ in suggested_articles] if suggested_articles else []
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
return response, metadata
|
| 103 |
-
|
| 104 |
-
# =============================================================================
|
| 105 |
-
# RAG-POWERED RESPONSE
|
| 106 |
-
# =============================================================================
|
| 107 |
-
|
| 108 |
-
def _generate_rag_response(
|
| 109 |
-
self,
|
| 110 |
-
message: str
|
| 111 |
-
) -> Tuple[str, float, list]:
|
| 112 |
-
"""
|
| 113 |
-
Generate response using RAG from knowledge base
|
| 114 |
-
|
| 115 |
-
Returns:
|
| 116 |
-
Tuple of (response, confidence, suggested_articles)
|
| 117 |
-
"""
|
| 118 |
-
# Search knowledge base for relevant articles
|
| 119 |
-
relevant_articles = self.kb_search.suggest_articles_for_ticket(
|
| 120 |
-
message,
|
| 121 |
-
top_k=3
|
| 122 |
-
)
|
| 123 |
-
|
| 124 |
-
if not relevant_articles:
|
| 125 |
-
# No relevant articles found
|
| 126 |
-
return self._handle_unknown_query(), 0.3, []
|
| 127 |
-
|
| 128 |
-
# Get best match
|
| 129 |
-
best_article, best_score = relevant_articles[0]
|
| 130 |
-
|
| 131 |
-
# If confidence is too low, escalate
|
| 132 |
-
if best_score < 0.4:
|
| 133 |
-
return self._handle_unknown_query(), best_score, relevant_articles
|
| 134 |
-
|
| 135 |
-
# Build response from article
|
| 136 |
-
response = self._build_response_from_article(best_article, message)
|
| 137 |
-
|
| 138 |
-
# Add suggestions for other articles
|
| 139 |
-
if len(relevant_articles) > 1:
|
| 140 |
-
response += "\n\nYou might also find these helpful:\n"
|
| 141 |
-
for article, score in relevant_articles[1:]:
|
| 142 |
-
response += f"• {article.title}\n"
|
| 143 |
-
|
| 144 |
-
return response, best_score, relevant_articles
|
| 145 |
-
|
| 146 |
-
def _build_response_from_article(
|
| 147 |
-
self,
|
| 148 |
-
article,
|
| 149 |
-
query: str
|
| 150 |
-
) -> str:
|
| 151 |
-
"""Build a conversational response from KB article"""
|
| 152 |
-
# Extract relevant portion of article
|
| 153 |
-
content = article.content
|
| 154 |
-
|
| 155 |
-
# Try to find the most relevant paragraph
|
| 156 |
-
paragraphs = content.split('\n\n')
|
| 157 |
-
|
| 158 |
-
# Simple relevance: paragraph containing query keywords
|
| 159 |
-
query_words = set(query.lower().split())
|
| 160 |
-
relevant_para = None
|
| 161 |
-
max_overlap = 0
|
| 162 |
-
|
| 163 |
-
for para in paragraphs:
|
| 164 |
-
para_words = set(para.lower().split())
|
| 165 |
-
overlap = len(query_words & para_words)
|
| 166 |
-
if overlap > max_overlap:
|
| 167 |
-
max_overlap = overlap
|
| 168 |
-
relevant_para = para
|
| 169 |
-
|
| 170 |
-
if relevant_para:
|
| 171 |
-
response = f"Based on our knowledge base, here's what I found:\n\n{relevant_para}\n\n"
|
| 172 |
-
else:
|
| 173 |
-
# Use summary or first paragraph
|
| 174 |
-
response = f"{article.summary or paragraphs[0] if paragraphs else content[:300]}\n\n"
|
| 175 |
-
|
| 176 |
-
response += f"For more details, check out: **{article.title}**"
|
| 177 |
-
|
| 178 |
-
return response
|
| 179 |
-
|
| 180 |
-
def _handle_unknown_query(self) -> str:
|
| 181 |
-
"""Handle queries with no good KB match"""
|
| 182 |
-
return (
|
| 183 |
-
"I'm not entirely sure about that. Let me connect you with one of our support agents "
|
| 184 |
-
"who can help you better. Would you like me to do that?"
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
# =============================================================================
|
| 188 |
-
# INTENT & SENTIMENT DETECTION
|
| 189 |
-
# =============================================================================
|
| 190 |
-
|
| 191 |
-
def detect_intent(self, message: str) -> str:
|
| 192 |
-
"""
|
| 193 |
-
Detect user intent
|
| 194 |
-
|
| 195 |
-
Returns:
|
| 196 |
-
greeting, farewell, question, complaint, escalation_request, general
|
| 197 |
-
"""
|
| 198 |
-
message_lower = message.lower()
|
| 199 |
-
|
| 200 |
-
# Greeting
|
| 201 |
-
if any(re.search(pattern, message_lower) for pattern in self.greeting_patterns):
|
| 202 |
-
return 'greeting'
|
| 203 |
-
|
| 204 |
-
# Farewell
|
| 205 |
-
if any(word in message_lower for word in ['bye', 'goodbye', 'thanks', 'thank you']):
|
| 206 |
-
return 'farewell'
|
| 207 |
-
|
| 208 |
-
# Escalation request
|
| 209 |
-
if any(keyword in message_lower for keyword in self.escalation_keywords):
|
| 210 |
-
return 'escalation_request'
|
| 211 |
-
|
| 212 |
-
# Question (contains question words or ?)
|
| 213 |
-
if '?' in message or any(word in message_lower for word in ['how', 'what', 'why', 'when', 'where', 'can', 'could', 'would']):
|
| 214 |
-
return 'question'
|
| 215 |
-
|
| 216 |
-
# Complaint
|
| 217 |
-
complaint_words = ['problem', 'issue', 'error', 'broken', 'not working', 'wrong']
|
| 218 |
-
if any(word in message_lower for word in complaint_words):
|
| 219 |
-
return 'complaint'
|
| 220 |
-
|
| 221 |
-
return 'general'
|
| 222 |
-
|
| 223 |
-
def detect_sentiment(self, message: str) -> str:
|
| 224 |
-
"""
|
| 225 |
-
Detect sentiment
|
| 226 |
-
|
| 227 |
-
Returns:
|
| 228 |
-
positive, neutral, negative
|
| 229 |
-
"""
|
| 230 |
-
message_lower = message.lower()
|
| 231 |
-
|
| 232 |
-
# Negative keywords
|
| 233 |
-
negative_keywords = [
|
| 234 |
-
'angry', 'frustrated', 'terrible', 'awful', 'worst', 'hate',
|
| 235 |
-
'disappointed', 'useless', 'horrible', 'bad', 'problem', 'issue'
|
| 236 |
-
]
|
| 237 |
-
|
| 238 |
-
# Positive keywords
|
| 239 |
-
positive_keywords = [
|
| 240 |
-
'great', 'excellent', 'awesome', 'wonderful', 'love', 'perfect',
|
| 241 |
-
'thanks', 'thank you', 'appreciate', 'helpful', 'good'
|
| 242 |
-
]
|
| 243 |
-
|
| 244 |
-
neg_count = sum(1 for word in negative_keywords if word in message_lower)
|
| 245 |
-
pos_count = sum(1 for word in positive_keywords if word in message_lower)
|
| 246 |
-
|
| 247 |
-
if neg_count > pos_count:
|
| 248 |
-
return 'negative'
|
| 249 |
-
elif pos_count > neg_count:
|
| 250 |
-
return 'positive'
|
| 251 |
-
else:
|
| 252 |
-
return 'neutral'
|
| 253 |
-
|
| 254 |
-
def should_escalate_to_human(
|
| 255 |
-
self,
|
| 256 |
-
message: str,
|
| 257 |
-
sentiment: str,
|
| 258 |
-
intent: str
|
| 259 |
-
) -> bool:
|
| 260 |
-
"""
|
| 261 |
-
Determine if conversation should be escalated to human
|
| 262 |
-
|
| 263 |
-
Returns:
|
| 264 |
-
True if should escalate
|
| 265 |
-
"""
|
| 266 |
-
message_lower = message.lower()
|
| 267 |
-
|
| 268 |
-
# Explicit escalation request
|
| 269 |
-
if intent == 'escalation_request':
|
| 270 |
-
return True
|
| 271 |
-
|
| 272 |
-
# Very negative sentiment
|
| 273 |
-
if sentiment == 'negative':
|
| 274 |
-
angry_words = ['angry', 'furious', 'hate', 'worst']
|
| 275 |
-
if any(word in message_lower for word in angry_words):
|
| 276 |
-
return True
|
| 277 |
-
|
| 278 |
-
# Billing/payment issues
|
| 279 |
-
if any(word in message_lower for word in ['refund', 'charge', 'billing', 'payment']):
|
| 280 |
-
return True
|
| 281 |
-
|
| 282 |
-
# Account security
|
| 283 |
-
if any(word in message_lower for word in ['hacked', 'unauthorized', 'fraud', 'security']):
|
| 284 |
-
return True
|
| 285 |
-
|
| 286 |
-
return False
|
| 287 |
-
|
| 288 |
-
# =============================================================================
|
| 289 |
-
# PREDEFINED RESPONSES
|
| 290 |
-
# =============================================================================
|
| 291 |
-
|
| 292 |
-
def _handle_greeting(self, message: str) -> str:
|
| 293 |
-
"""Handle greeting"""
|
| 294 |
-
return (
|
| 295 |
-
"Hello! 👋 I'm your AI support assistant. "
|
| 296 |
-
"I'm here to help you with any questions you have. "
|
| 297 |
-
"What can I assist you with today?"
|
| 298 |
-
)
|
| 299 |
-
|
| 300 |
-
def _handle_farewell(self, message: str) -> str:
|
| 301 |
-
"""Handle goodbye"""
|
| 302 |
-
if 'thank' in message.lower():
|
| 303 |
-
return (
|
| 304 |
-
"You're very welcome! 😊 If you need any more help, feel free to start a new chat. "
|
| 305 |
-
"Have a great day!"
|
| 306 |
-
)
|
| 307 |
-
else:
|
| 308 |
-
return "Goodbye! Feel free to reach out if you need anything else. Take care! 👋"
|
| 309 |
-
|
| 310 |
-
def _handle_escalation(self, message: str) -> str:
|
| 311 |
-
"""Handle escalation request"""
|
| 312 |
-
return (
|
| 313 |
-
"I understand. Let me connect you with one of our support specialists "
|
| 314 |
-
"who can assist you further. Please hold on for just a moment..."
|
| 315 |
-
)
|
| 316 |
-
|
| 317 |
-
# =============================================================================
|
| 318 |
-
# UTILITIES
|
| 319 |
-
# =============================================================================
|
| 320 |
-
|
| 321 |
-
def get_suggested_responses(
|
| 322 |
-
self,
|
| 323 |
-
message: str
|
| 324 |
-
) -> list:
|
| 325 |
-
"""
|
| 326 |
-
Get suggested quick responses for agents
|
| 327 |
-
|
| 328 |
-
Returns:
|
| 329 |
-
List of suggested response texts
|
| 330 |
-
"""
|
| 331 |
-
intent = self.detect_intent(message)
|
| 332 |
-
|
| 333 |
-
suggestions = {
|
| 334 |
-
'greeting': [
|
| 335 |
-
"Hello! How can I help you today?",
|
| 336 |
-
"Hi there! What brings you here today?",
|
| 337 |
-
"Welcome! How may I assist you?"
|
| 338 |
-
],
|
| 339 |
-
'farewell': [
|
| 340 |
-
"Thank you for contacting us! Have a great day!",
|
| 341 |
-
"You're welcome! Feel free to reach out anytime.",
|
| 342 |
-
"Glad I could help! Take care!"
|
| 343 |
-
],
|
| 344 |
-
'question': [
|
| 345 |
-
"Let me look that up for you...",
|
| 346 |
-
"I can help you with that. Here's what I found...",
|
| 347 |
-
"Great question! Let me explain..."
|
| 348 |
-
],
|
| 349 |
-
'complaint': [
|
| 350 |
-
"I'm sorry to hear you're experiencing this issue. Let me help...",
|
| 351 |
-
"I understand your frustration. Let's resolve this...",
|
| 352 |
-
"Thank you for bringing this to our attention. Let me assist..."
|
| 353 |
-
]
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
return suggestions.get(intent, [
|
| 357 |
-
"How can I assist you with that?",
|
| 358 |
-
"Let me help you with that.",
|
| 359 |
-
"I'm here to help!"
|
| 360 |
-
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,427 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Chat Manager - Session and message handling for live chat
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
from typing import Optional, List, Dict, Any, Tuple
|
| 6 |
-
from datetime import datetime, timedelta
|
| 7 |
-
from sqlalchemy.orm import Session
|
| 8 |
-
from sqlalchemy import desc, and_
|
| 9 |
-
import uuid
|
| 10 |
-
import json
|
| 11 |
-
|
| 12 |
-
from models.cx_models import CXChatSession, CXChatMessage, CXCustomer, CXInteraction
|
| 13 |
-
from database.manager import get_db_manager
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
class ChatManager:
|
| 17 |
-
"""Manages live chat sessions and messages"""
|
| 18 |
-
|
| 19 |
-
def __init__(self):
|
| 20 |
-
self.db = get_db_manager()
|
| 21 |
-
|
| 22 |
-
# =============================================================================
|
| 23 |
-
# SESSION MANAGEMENT
|
| 24 |
-
# =============================================================================
|
| 25 |
-
|
| 26 |
-
def create_session(
|
| 27 |
-
self,
|
| 28 |
-
customer_id: Optional[int] = None,
|
| 29 |
-
page_url: Optional[str] = None,
|
| 30 |
-
referrer: Optional[str] = None,
|
| 31 |
-
user_agent: Optional[str] = None,
|
| 32 |
-
ip_address: Optional[str] = None
|
| 33 |
-
) -> CXChatSession:
|
| 34 |
-
"""
|
| 35 |
-
Create a new chat session
|
| 36 |
-
|
| 37 |
-
Args:
|
| 38 |
-
customer_id: Optional customer ID if identified
|
| 39 |
-
page_url: Page where chat was initiated
|
| 40 |
-
referrer: Referrer URL
|
| 41 |
-
user_agent: Browser user agent
|
| 42 |
-
ip_address: Client IP address
|
| 43 |
-
|
| 44 |
-
Returns:
|
| 45 |
-
Created session
|
| 46 |
-
"""
|
| 47 |
-
with self.db.get_session() as session:
|
| 48 |
-
# Generate unique session ID
|
| 49 |
-
session_id = str(uuid.uuid4())
|
| 50 |
-
|
| 51 |
-
chat_session = CXChatSession(
|
| 52 |
-
customer_id=customer_id,
|
| 53 |
-
session_id=session_id,
|
| 54 |
-
status='active',
|
| 55 |
-
bot_active=True,
|
| 56 |
-
page_url=page_url,
|
| 57 |
-
referrer=referrer,
|
| 58 |
-
user_agent=user_agent,
|
| 59 |
-
ip_address=ip_address
|
| 60 |
-
)
|
| 61 |
-
|
| 62 |
-
session.add(chat_session)
|
| 63 |
-
session.commit()
|
| 64 |
-
session.refresh(chat_session)
|
| 65 |
-
|
| 66 |
-
return chat_session
|
| 67 |
-
|
| 68 |
-
def get_session(self, session_id: str) -> Optional[CXChatSession]:
|
| 69 |
-
"""Get session by session_id"""
|
| 70 |
-
with self.db.get_session() as session:
|
| 71 |
-
chat_session = session.query(CXChatSession).filter_by(
|
| 72 |
-
session_id=session_id
|
| 73 |
-
).first()
|
| 74 |
-
|
| 75 |
-
if chat_session:
|
| 76 |
-
_ = chat_session.customer # Eager load
|
| 77 |
-
_ = chat_session.messages
|
| 78 |
-
|
| 79 |
-
return chat_session
|
| 80 |
-
|
| 81 |
-
def get_session_by_id(self, id: int) -> Optional[CXChatSession]:
|
| 82 |
-
"""Get session by database ID"""
|
| 83 |
-
with self.db.get_session() as session:
|
| 84 |
-
return session.query(CXChatSession).filter_by(id=id).first()
|
| 85 |
-
|
| 86 |
-
def get_all_sessions(
|
| 87 |
-
self,
|
| 88 |
-
status: Optional[str] = None,
|
| 89 |
-
assigned_to: Optional[str] = None,
|
| 90 |
-
limit: int = 100,
|
| 91 |
-
offset: int = 0
|
| 92 |
-
) -> Tuple[List[CXChatSession], int]:
|
| 93 |
-
"""
|
| 94 |
-
Get all sessions with filters
|
| 95 |
-
|
| 96 |
-
Returns:
|
| 97 |
-
Tuple of (sessions list, total count)
|
| 98 |
-
"""
|
| 99 |
-
with self.db.get_session() as session:
|
| 100 |
-
query = session.query(CXChatSession)
|
| 101 |
-
|
| 102 |
-
if status:
|
| 103 |
-
query = query.filter(CXChatSession.status == status)
|
| 104 |
-
if assigned_to:
|
| 105 |
-
query = query.filter(CXChatSession.assigned_to == assigned_to)
|
| 106 |
-
|
| 107 |
-
total = query.count()
|
| 108 |
-
|
| 109 |
-
sessions = query.order_by(desc(CXChatSession.started_at)).limit(limit).offset(offset).all()
|
| 110 |
-
|
| 111 |
-
# Eager load customers
|
| 112 |
-
for chat_session in sessions:
|
| 113 |
-
_ = chat_session.customer
|
| 114 |
-
|
| 115 |
-
return sessions, total
|
| 116 |
-
|
| 117 |
-
def update_session(self, session_id: str, **updates) -> CXChatSession:
|
| 118 |
-
"""Update session fields"""
|
| 119 |
-
with self.db.get_session() as session:
|
| 120 |
-
chat_session = session.query(CXChatSession).filter_by(
|
| 121 |
-
session_id=session_id
|
| 122 |
-
).first()
|
| 123 |
-
|
| 124 |
-
if not chat_session:
|
| 125 |
-
raise ValueError(f"Session {session_id} not found")
|
| 126 |
-
|
| 127 |
-
for key, value in updates.items():
|
| 128 |
-
if hasattr(chat_session, key):
|
| 129 |
-
setattr(chat_session, key, value)
|
| 130 |
-
|
| 131 |
-
# Handle status changes
|
| 132 |
-
if 'status' in updates and updates['status'] == 'closed':
|
| 133 |
-
chat_session.ended_at = datetime.utcnow()
|
| 134 |
-
|
| 135 |
-
session.commit()
|
| 136 |
-
session.refresh(chat_session)
|
| 137 |
-
|
| 138 |
-
return chat_session
|
| 139 |
-
|
| 140 |
-
def close_session(
|
| 141 |
-
self,
|
| 142 |
-
session_id: str,
|
| 143 |
-
rating: Optional[int] = None,
|
| 144 |
-
feedback: Optional[str] = None
|
| 145 |
-
) -> CXChatSession:
|
| 146 |
-
"""Close a chat session"""
|
| 147 |
-
with self.db.get_session() as session:
|
| 148 |
-
chat_session = session.query(CXChatSession).filter_by(
|
| 149 |
-
session_id=session_id
|
| 150 |
-
).first()
|
| 151 |
-
|
| 152 |
-
if not chat_session:
|
| 153 |
-
raise ValueError(f"Session {session_id} not found")
|
| 154 |
-
|
| 155 |
-
chat_session.status = 'closed'
|
| 156 |
-
chat_session.ended_at = datetime.utcnow()
|
| 157 |
-
|
| 158 |
-
if rating is not None:
|
| 159 |
-
chat_session.rated = True
|
| 160 |
-
chat_session.rating = rating
|
| 161 |
-
chat_session.feedback = feedback
|
| 162 |
-
|
| 163 |
-
# Create interaction record
|
| 164 |
-
if chat_session.customer_id:
|
| 165 |
-
interaction = CXInteraction(
|
| 166 |
-
customer_id=chat_session.customer_id,
|
| 167 |
-
type='chat',
|
| 168 |
-
channel='web',
|
| 169 |
-
summary=f"Chat session - {chat_session.message_count} messages",
|
| 170 |
-
reference_type='chat_session',
|
| 171 |
-
reference_id=chat_session.id,
|
| 172 |
-
satisfaction_rating=rating,
|
| 173 |
-
occurred_at=chat_session.started_at
|
| 174 |
-
)
|
| 175 |
-
session.add(interaction)
|
| 176 |
-
|
| 177 |
-
session.commit()
|
| 178 |
-
session.refresh(chat_session)
|
| 179 |
-
|
| 180 |
-
return chat_session
|
| 181 |
-
|
| 182 |
-
# =============================================================================
|
| 183 |
-
# MESSAGE MANAGEMENT
|
| 184 |
-
# =============================================================================
|
| 185 |
-
|
| 186 |
-
def add_message(
|
| 187 |
-
self,
|
| 188 |
-
session_id: str,
|
| 189 |
-
sender_type: str,
|
| 190 |
-
message: str,
|
| 191 |
-
sender_id: Optional[str] = None,
|
| 192 |
-
sender_name: Optional[str] = None,
|
| 193 |
-
message_type: str = 'text',
|
| 194 |
-
is_bot_response: bool = False,
|
| 195 |
-
bot_confidence: Optional[float] = None,
|
| 196 |
-
intent: Optional[str] = None
|
| 197 |
-
) -> CXChatMessage:
|
| 198 |
-
"""
|
| 199 |
-
Add a message to a chat session
|
| 200 |
-
|
| 201 |
-
Args:
|
| 202 |
-
session_id: Session ID
|
| 203 |
-
sender_type: customer, agent, bot, system
|
| 204 |
-
message: Message content
|
| 205 |
-
sender_id: Optional sender ID
|
| 206 |
-
sender_name: Display name
|
| 207 |
-
message_type: text, image, file, system_message
|
| 208 |
-
is_bot_response: Whether this is from AI bot
|
| 209 |
-
bot_confidence: Bot confidence score (0-1)
|
| 210 |
-
intent: Detected intent
|
| 211 |
-
|
| 212 |
-
Returns:
|
| 213 |
-
Created message
|
| 214 |
-
"""
|
| 215 |
-
with self.db.get_session() as session:
|
| 216 |
-
# Get chat session
|
| 217 |
-
chat_session = session.query(CXChatSession).filter_by(
|
| 218 |
-
session_id=session_id
|
| 219 |
-
).first()
|
| 220 |
-
|
| 221 |
-
if not chat_session:
|
| 222 |
-
raise ValueError(f"Session {session_id} not found")
|
| 223 |
-
|
| 224 |
-
# Create message
|
| 225 |
-
chat_message = CXChatMessage(
|
| 226 |
-
session_id=chat_session.id,
|
| 227 |
-
sender_type=sender_type,
|
| 228 |
-
sender_id=sender_id,
|
| 229 |
-
sender_name=sender_name,
|
| 230 |
-
message=message,
|
| 231 |
-
message_type=message_type,
|
| 232 |
-
is_bot_response=is_bot_response,
|
| 233 |
-
bot_confidence=bot_confidence,
|
| 234 |
-
intent=intent
|
| 235 |
-
)
|
| 236 |
-
|
| 237 |
-
session.add(chat_message)
|
| 238 |
-
|
| 239 |
-
# Update session
|
| 240 |
-
chat_session.message_count += 1
|
| 241 |
-
|
| 242 |
-
# Update wait time if first agent response
|
| 243 |
-
if sender_type == 'agent' and chat_session.wait_time_seconds == 0:
|
| 244 |
-
delta = datetime.utcnow() - chat_session.started_at
|
| 245 |
-
chat_session.wait_time_seconds = int(delta.total_seconds())
|
| 246 |
-
|
| 247 |
-
session.commit()
|
| 248 |
-
session.refresh(chat_message)
|
| 249 |
-
|
| 250 |
-
return chat_message
|
| 251 |
-
|
| 252 |
-
def get_session_messages(
|
| 253 |
-
self,
|
| 254 |
-
session_id: str,
|
| 255 |
-
limit: Optional[int] = None
|
| 256 |
-
) -> List[CXChatMessage]:
|
| 257 |
-
"""Get all messages for a session"""
|
| 258 |
-
with self.db.get_session() as session:
|
| 259 |
-
# Get session by session_id
|
| 260 |
-
chat_session = session.query(CXChatSession).filter_by(
|
| 261 |
-
session_id=session_id
|
| 262 |
-
).first()
|
| 263 |
-
|
| 264 |
-
if not chat_session:
|
| 265 |
-
return []
|
| 266 |
-
|
| 267 |
-
query = session.query(CXChatMessage).filter(
|
| 268 |
-
CXChatMessage.session_id == chat_session.id
|
| 269 |
-
).order_by(CXChatMessage.created_at)
|
| 270 |
-
|
| 271 |
-
if limit:
|
| 272 |
-
query = query.limit(limit)
|
| 273 |
-
|
| 274 |
-
return query.all()
|
| 275 |
-
|
| 276 |
-
def mark_message_read(self, message_id: int):
|
| 277 |
-
"""Mark a message as read"""
|
| 278 |
-
with self.db.get_session() as session:
|
| 279 |
-
message = session.query(CXChatMessage).filter_by(id=message_id).first()
|
| 280 |
-
if message:
|
| 281 |
-
message.is_read = True
|
| 282 |
-
message.read_at = datetime.utcnow()
|
| 283 |
-
session.commit()
|
| 284 |
-
|
| 285 |
-
# =============================================================================
|
| 286 |
-
# ASSIGNMENT & ROUTING
|
| 287 |
-
# =============================================================================
|
| 288 |
-
|
| 289 |
-
def assign_to_agent(
|
| 290 |
-
self,
|
| 291 |
-
session_id: str,
|
| 292 |
-
agent_id: str,
|
| 293 |
-
agent_name: Optional[str] = None
|
| 294 |
-
) -> CXChatSession:
|
| 295 |
-
"""Assign chat session to an agent"""
|
| 296 |
-
with self.db.get_session() as session:
|
| 297 |
-
chat_session = session.query(CXChatSession).filter_by(
|
| 298 |
-
session_id=session_id
|
| 299 |
-
).first()
|
| 300 |
-
|
| 301 |
-
if not chat_session:
|
| 302 |
-
raise ValueError(f"Session {session_id} not found")
|
| 303 |
-
|
| 304 |
-
chat_session.assigned_to = agent_id
|
| 305 |
-
chat_session.assigned_at = datetime.utcnow()
|
| 306 |
-
chat_session.status = 'assigned'
|
| 307 |
-
|
| 308 |
-
# Add system message
|
| 309 |
-
system_msg = CXChatMessage(
|
| 310 |
-
session_id=chat_session.id,
|
| 311 |
-
sender_type='system',
|
| 312 |
-
sender_name='System',
|
| 313 |
-
message=f"Chat assigned to {agent_name or agent_id}",
|
| 314 |
-
message_type='system_message'
|
| 315 |
-
)
|
| 316 |
-
session.add(system_msg)
|
| 317 |
-
|
| 318 |
-
session.commit()
|
| 319 |
-
session.refresh(chat_session)
|
| 320 |
-
|
| 321 |
-
return chat_session
|
| 322 |
-
|
| 323 |
-
def handoff_to_human(
|
| 324 |
-
self,
|
| 325 |
-
session_id: str,
|
| 326 |
-
reason: Optional[str] = None
|
| 327 |
-
) -> CXChatSession:
|
| 328 |
-
"""
|
| 329 |
-
Handoff from bot to human agent
|
| 330 |
-
|
| 331 |
-
Args:
|
| 332 |
-
session_id: Session ID
|
| 333 |
-
reason: Reason for handoff (escalation, unable_to_help, customer_request)
|
| 334 |
-
"""
|
| 335 |
-
with self.db.get_session() as session:
|
| 336 |
-
chat_session = session.query(CXChatSession).filter_by(
|
| 337 |
-
session_id=session_id
|
| 338 |
-
).first()
|
| 339 |
-
|
| 340 |
-
if not chat_session:
|
| 341 |
-
raise ValueError(f"Session {session_id} not found")
|
| 342 |
-
|
| 343 |
-
chat_session.bot_handed_off = True
|
| 344 |
-
chat_session.bot_handoff_reason = reason
|
| 345 |
-
chat_session.bot_active = False
|
| 346 |
-
chat_session.status = 'waiting' # Waiting for agent
|
| 347 |
-
|
| 348 |
-
# Add system message
|
| 349 |
-
system_msg = CXChatMessage(
|
| 350 |
-
session_id=chat_session.id,
|
| 351 |
-
sender_type='system',
|
| 352 |
-
sender_name='System',
|
| 353 |
-
message="Transferring you to a human agent...",
|
| 354 |
-
message_type='system_message'
|
| 355 |
-
)
|
| 356 |
-
session.add(system_msg)
|
| 357 |
-
|
| 358 |
-
session.commit()
|
| 359 |
-
session.refresh(chat_session)
|
| 360 |
-
|
| 361 |
-
return chat_session
|
| 362 |
-
|
| 363 |
-
# =============================================================================
|
| 364 |
-
# ANALYTICS
|
| 365 |
-
# =============================================================================
|
| 366 |
-
|
| 367 |
-
def get_chat_stats(
|
| 368 |
-
self,
|
| 369 |
-
start_date: Optional[datetime] = None,
|
| 370 |
-
end_date: Optional[datetime] = None
|
| 371 |
-
) -> Dict[str, Any]:
|
| 372 |
-
"""Get chat statistics"""
|
| 373 |
-
with self.db.get_session() as session:
|
| 374 |
-
query = session.query(CXChatSession)
|
| 375 |
-
|
| 376 |
-
if start_date:
|
| 377 |
-
query = query.filter(CXChatSession.started_at >= start_date)
|
| 378 |
-
if end_date:
|
| 379 |
-
query = query.filter(CXChatSession.started_at <= end_date)
|
| 380 |
-
|
| 381 |
-
sessions = query.all()
|
| 382 |
-
|
| 383 |
-
total = len(sessions)
|
| 384 |
-
by_status = {}
|
| 385 |
-
wait_times = []
|
| 386 |
-
ratings = []
|
| 387 |
-
bot_handoffs = 0
|
| 388 |
-
|
| 389 |
-
for chat_session in sessions:
|
| 390 |
-
# Count by status
|
| 391 |
-
by_status[chat_session.status] = by_status.get(chat_session.status, 0) + 1
|
| 392 |
-
|
| 393 |
-
# Collect wait times
|
| 394 |
-
if chat_session.wait_time_seconds:
|
| 395 |
-
wait_times.append(chat_session.wait_time_seconds)
|
| 396 |
-
|
| 397 |
-
# Collect ratings
|
| 398 |
-
if chat_session.rated and chat_session.rating:
|
| 399 |
-
ratings.append(chat_session.rating)
|
| 400 |
-
|
| 401 |
-
# Count bot handoffs
|
| 402 |
-
if chat_session.bot_handed_off:
|
| 403 |
-
bot_handoffs += 1
|
| 404 |
-
|
| 405 |
-
return {
|
| 406 |
-
'total_sessions': total,
|
| 407 |
-
'by_status': by_status,
|
| 408 |
-
'avg_wait_time_seconds': sum(wait_times) / len(wait_times) if wait_times else 0,
|
| 409 |
-
'avg_rating': sum(ratings) / len(ratings) if ratings else 0,
|
| 410 |
-
'bot_handoff_rate': bot_handoffs / total if total > 0 else 0
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
def get_active_sessions_count(self) -> int:
|
| 414 |
-
"""Get count of active sessions"""
|
| 415 |
-
with self.db.get_session() as session:
|
| 416 |
-
return session.query(CXChatSession).filter(
|
| 417 |
-
CXChatSession.status.in_(['active', 'waiting', 'assigned'])
|
| 418 |
-
).count()
|
| 419 |
-
|
| 420 |
-
def get_waiting_sessions(self) -> List[CXChatSession]:
|
| 421 |
-
"""Get sessions waiting for agent assignment"""
|
| 422 |
-
with self.db.get_session() as session:
|
| 423 |
-
sessions = session.query(CXChatSession).filter(
|
| 424 |
-
CXChatSession.status == 'waiting'
|
| 425 |
-
).order_by(CXChatSession.started_at).all()
|
| 426 |
-
|
| 427 |
-
return sessions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,331 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Chat UI - Gradio interface for managing chat sessions
|
| 3 |
-
Note: Real-time chat would require WebSockets; this is a management interface
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import gradio as gr
|
| 7 |
-
from typing import List
|
| 8 |
-
from datetime import datetime
|
| 9 |
-
|
| 10 |
-
from .manager import ChatManager
|
| 11 |
-
from .bot import ChatBot
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
class ChatUI:
|
| 15 |
-
"""Gradio UI for chat management"""
|
| 16 |
-
|
| 17 |
-
def __init__(self):
|
| 18 |
-
self.manager = ChatManager()
|
| 19 |
-
self.bot = ChatBot()
|
| 20 |
-
|
| 21 |
-
def render(self) -> gr.Blocks:
|
| 22 |
-
"""Render chat management interface"""
|
| 23 |
-
|
| 24 |
-
with gr.Blocks() as chat_interface:
|
| 25 |
-
gr.Markdown("# 💬 Live Chat Management")
|
| 26 |
-
gr.Markdown("Monitor and manage customer chat sessions")
|
| 27 |
-
|
| 28 |
-
with gr.Tabs():
|
| 29 |
-
# TAB 1: Active Sessions
|
| 30 |
-
with gr.Tab("🟢 Active Sessions"):
|
| 31 |
-
refresh_active_btn = gr.Button("🔄 Refresh", variant="secondary")
|
| 32 |
-
|
| 33 |
-
active_sessions_table = gr.Dataframe(
|
| 34 |
-
headers=["ID", "Customer", "Status", "Bot Active", "Messages", "Wait Time", "Started", "Assigned To"],
|
| 35 |
-
label="Active Chat Sessions",
|
| 36 |
-
interactive=False
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
with gr.Row():
|
| 40 |
-
active_count = gr.Textbox(label="Active Sessions", value="0")
|
| 41 |
-
waiting_count = gr.Textbox(label="Waiting for Agent", value="0")
|
| 42 |
-
avg_wait_time = gr.Textbox(label="Avg Wait Time", value="0s")
|
| 43 |
-
|
| 44 |
-
# TAB 2: Session Details
|
| 45 |
-
with gr.Tab("🔍 Session Details"):
|
| 46 |
-
with gr.Row():
|
| 47 |
-
session_id_input = gr.Textbox(label="Session ID")
|
| 48 |
-
load_session_btn = gr.Button("Load Session", variant="secondary")
|
| 49 |
-
|
| 50 |
-
session_info = gr.Markdown("Enter a session ID to view details")
|
| 51 |
-
|
| 52 |
-
gr.Markdown("### 💬 Conversation")
|
| 53 |
-
conversation_display = gr.Chatbot(
|
| 54 |
-
label="Messages",
|
| 55 |
-
height=400
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
with gr.Row():
|
| 59 |
-
with gr.Column(scale=3):
|
| 60 |
-
agent_message_input = gr.Textbox(
|
| 61 |
-
label="Send Message as Agent",
|
| 62 |
-
placeholder="Type your response...",
|
| 63 |
-
lines=2
|
| 64 |
-
)
|
| 65 |
-
with gr.Column(scale=1):
|
| 66 |
-
send_message_btn = gr.Button("Send", variant="primary")
|
| 67 |
-
handoff_btn = gr.Button("🤖→👤 Handoff to Human")
|
| 68 |
-
close_session_btn = gr.Button("🔒 Close Session")
|
| 69 |
-
|
| 70 |
-
action_output = gr.Textbox(label="Action Result", lines=2)
|
| 71 |
-
|
| 72 |
-
# TAB 3: Bot Testing
|
| 73 |
-
with gr.Tab("🤖 Test Bot"):
|
| 74 |
-
gr.Markdown("### Test AI Bot Responses")
|
| 75 |
-
gr.Markdown("See how the bot would respond to customer messages")
|
| 76 |
-
|
| 77 |
-
test_message_input = gr.Textbox(
|
| 78 |
-
label="Test Message",
|
| 79 |
-
placeholder="How do I reset my password?",
|
| 80 |
-
lines=2
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
test_bot_btn = gr.Button("Test Bot Response", variant="primary")
|
| 84 |
-
|
| 85 |
-
bot_response_output = gr.Markdown("Bot response will appear here")
|
| 86 |
-
|
| 87 |
-
bot_metadata_output = gr.JSON(label="Response Metadata")
|
| 88 |
-
|
| 89 |
-
# TAB 4: Chat Analytics
|
| 90 |
-
with gr.Tab("📊 Analytics"):
|
| 91 |
-
gr.Markdown("### Chat Performance Metrics")
|
| 92 |
-
|
| 93 |
-
refresh_stats_btn = gr.Button("🔄 Refresh Stats", variant="secondary")
|
| 94 |
-
|
| 95 |
-
with gr.Row():
|
| 96 |
-
total_sessions_metric = gr.Textbox(label="Total Sessions", value="0")
|
| 97 |
-
avg_rating_metric = gr.Textbox(label="Avg Rating", value="0.0")
|
| 98 |
-
handoff_rate_metric = gr.Textbox(label="Bot Handoff Rate", value="0%")
|
| 99 |
-
|
| 100 |
-
chat_stats_json = gr.JSON(label="Detailed Statistics")
|
| 101 |
-
|
| 102 |
-
# =============================================================================
|
| 103 |
-
# EVENT HANDLERS
|
| 104 |
-
# =============================================================================
|
| 105 |
-
|
| 106 |
-
def load_active_sessions():
|
| 107 |
-
"""Load active chat sessions"""
|
| 108 |
-
try:
|
| 109 |
-
sessions, total = self.manager.get_all_sessions(limit=100)
|
| 110 |
-
|
| 111 |
-
rows = []
|
| 112 |
-
active_cnt = 0
|
| 113 |
-
waiting_cnt = 0
|
| 114 |
-
wait_times = []
|
| 115 |
-
|
| 116 |
-
for session in sessions:
|
| 117 |
-
if session.status in ['active', 'waiting', 'assigned']:
|
| 118 |
-
active_cnt += 1
|
| 119 |
-
|
| 120 |
-
if session.status == 'waiting':
|
| 121 |
-
waiting_cnt += 1
|
| 122 |
-
|
| 123 |
-
if session.wait_time_seconds:
|
| 124 |
-
wait_times.append(session.wait_time_seconds)
|
| 125 |
-
|
| 126 |
-
# Format wait time
|
| 127 |
-
wait_str = f"{session.wait_time_seconds}s" if session.wait_time_seconds else "N/A"
|
| 128 |
-
|
| 129 |
-
rows.append([
|
| 130 |
-
session.id,
|
| 131 |
-
session.customer.full_name if session.customer else "Anonymous",
|
| 132 |
-
session.status,
|
| 133 |
-
"Yes" if session.bot_active else "No",
|
| 134 |
-
session.message_count,
|
| 135 |
-
wait_str,
|
| 136 |
-
session.started_at.strftime("%Y-%m-%d %H:%M") if session.started_at else "",
|
| 137 |
-
session.assigned_to or "Unassigned"
|
| 138 |
-
])
|
| 139 |
-
|
| 140 |
-
avg_wait = int(sum(wait_times) / len(wait_times)) if wait_times else 0
|
| 141 |
-
|
| 142 |
-
return (
|
| 143 |
-
rows,
|
| 144 |
-
str(active_cnt),
|
| 145 |
-
str(waiting_cnt),
|
| 146 |
-
f"{avg_wait}s"
|
| 147 |
-
)
|
| 148 |
-
|
| 149 |
-
except Exception as e:
|
| 150 |
-
return [], f"Error: {e}", "0", "0s"
|
| 151 |
-
|
| 152 |
-
def load_session_details(session_id):
|
| 153 |
-
"""Load session and conversation"""
|
| 154 |
-
if not session_id:
|
| 155 |
-
return "Please enter a session ID", []
|
| 156 |
-
|
| 157 |
-
try:
|
| 158 |
-
session = self.manager.get_session(session_id)
|
| 159 |
-
if not session:
|
| 160 |
-
return "❌ Session not found", []
|
| 161 |
-
|
| 162 |
-
# Build session info
|
| 163 |
-
info = f"""
|
| 164 |
-
### Session: {session.session_id}
|
| 165 |
-
|
| 166 |
-
**Customer:** {session.customer.full_name if session.customer else 'Anonymous'}
|
| 167 |
-
**Status:** {session.status}
|
| 168 |
-
**Started:** {session.started_at.strftime("%Y-%m-%d %H:%M:%S") if session.started_at else 'N/A'}
|
| 169 |
-
**Messages:** {session.message_count}
|
| 170 |
-
**Bot Active:** {'Yes' if session.bot_active else 'No'}
|
| 171 |
-
**Assigned To:** {session.assigned_to or 'Unassigned'}
|
| 172 |
-
**Rating:** {session.rating if session.rated else 'Not rated'}
|
| 173 |
-
"""
|
| 174 |
-
|
| 175 |
-
# Load messages
|
| 176 |
-
messages = self.manager.get_session_messages(session_id)
|
| 177 |
-
|
| 178 |
-
# Format for chatbot display
|
| 179 |
-
conversation = []
|
| 180 |
-
for msg in messages:
|
| 181 |
-
# Determine role
|
| 182 |
-
if msg.sender_type in ['customer']:
|
| 183 |
-
role = "user"
|
| 184 |
-
else:
|
| 185 |
-
role = "assistant"
|
| 186 |
-
|
| 187 |
-
# Add message
|
| 188 |
-
conversation.append([msg.message, None] if role == "user" else [None, msg.message])
|
| 189 |
-
|
| 190 |
-
return info, conversation
|
| 191 |
-
|
| 192 |
-
except Exception as e:
|
| 193 |
-
return f"❌ Error: {str(e)}", []
|
| 194 |
-
|
| 195 |
-
def send_agent_message(session_id, message):
|
| 196 |
-
"""Send message as agent"""
|
| 197 |
-
if not session_id or not message:
|
| 198 |
-
return "Please provide session ID and message"
|
| 199 |
-
|
| 200 |
-
try:
|
| 201 |
-
self.manager.add_message(
|
| 202 |
-
session_id=session_id,
|
| 203 |
-
sender_type='agent',
|
| 204 |
-
sender_id='agent_1',
|
| 205 |
-
sender_name='Support Agent',
|
| 206 |
-
message=message
|
| 207 |
-
)
|
| 208 |
-
return f"✅ Message sent successfully"
|
| 209 |
-
except Exception as e:
|
| 210 |
-
return f"❌ Error: {str(e)}"
|
| 211 |
-
|
| 212 |
-
def handoff_to_human(session_id):
|
| 213 |
-
"""Handoff session from bot to human"""
|
| 214 |
-
if not session_id:
|
| 215 |
-
return "Please provide session ID"
|
| 216 |
-
|
| 217 |
-
try:
|
| 218 |
-
self.manager.handoff_to_human(
|
| 219 |
-
session_id=session_id,
|
| 220 |
-
reason='manual_handoff'
|
| 221 |
-
)
|
| 222 |
-
return "✅ Session handed off to human agent queue"
|
| 223 |
-
except Exception as e:
|
| 224 |
-
return f"❌ Error: {str(e)}"
|
| 225 |
-
|
| 226 |
-
def close_chat_session(session_id):
|
| 227 |
-
"""Close a session"""
|
| 228 |
-
if not session_id:
|
| 229 |
-
return "Please provide session ID"
|
| 230 |
-
|
| 231 |
-
try:
|
| 232 |
-
self.manager.close_session(session_id)
|
| 233 |
-
return "✅ Session closed successfully"
|
| 234 |
-
except Exception as e:
|
| 235 |
-
return f"❌ Error: {str(e)}"
|
| 236 |
-
|
| 237 |
-
def test_bot_response(message):
|
| 238 |
-
"""Test bot response generation"""
|
| 239 |
-
if not message:
|
| 240 |
-
return "Please enter a test message", {}
|
| 241 |
-
|
| 242 |
-
try:
|
| 243 |
-
response, metadata = self.bot.generate_response(message)
|
| 244 |
-
|
| 245 |
-
# Format response
|
| 246 |
-
output = f"## Bot Response:\n\n{response}\n\n"
|
| 247 |
-
|
| 248 |
-
# Add suggestions if available
|
| 249 |
-
if metadata.get('suggested_articles'):
|
| 250 |
-
output += "\n### Suggested Articles (IDs):\n"
|
| 251 |
-
for article_id in metadata['suggested_articles']:
|
| 252 |
-
output += f"- Article #{article_id}\n"
|
| 253 |
-
|
| 254 |
-
return output, metadata
|
| 255 |
-
|
| 256 |
-
except Exception as e:
|
| 257 |
-
return f"❌ Error: {str(e)}", {}
|
| 258 |
-
|
| 259 |
-
def load_chat_stats():
|
| 260 |
-
"""Load chat statistics"""
|
| 261 |
-
try:
|
| 262 |
-
stats = self.manager.get_chat_stats()
|
| 263 |
-
|
| 264 |
-
total = stats.get('total_sessions', 0)
|
| 265 |
-
avg_rating = stats.get('avg_rating', 0)
|
| 266 |
-
handoff_rate = stats.get('bot_handoff_rate', 0)
|
| 267 |
-
|
| 268 |
-
return (
|
| 269 |
-
str(total),
|
| 270 |
-
f"{avg_rating:.1f}",
|
| 271 |
-
f"{int(handoff_rate * 100)}%",
|
| 272 |
-
stats
|
| 273 |
-
)
|
| 274 |
-
|
| 275 |
-
except Exception as e:
|
| 276 |
-
return "Error", "Error", "Error", {"error": str(e)}
|
| 277 |
-
|
| 278 |
-
# Wire up events
|
| 279 |
-
refresh_active_btn.click(
|
| 280 |
-
fn=load_active_sessions,
|
| 281 |
-
outputs=[active_sessions_table, active_count, waiting_count, avg_wait_time]
|
| 282 |
-
)
|
| 283 |
-
|
| 284 |
-
load_session_btn.click(
|
| 285 |
-
fn=load_session_details,
|
| 286 |
-
inputs=[session_id_input],
|
| 287 |
-
outputs=[session_info, conversation_display]
|
| 288 |
-
)
|
| 289 |
-
|
| 290 |
-
send_message_btn.click(
|
| 291 |
-
fn=send_agent_message,
|
| 292 |
-
inputs=[session_id_input, agent_message_input],
|
| 293 |
-
outputs=[action_output]
|
| 294 |
-
)
|
| 295 |
-
|
| 296 |
-
handoff_btn.click(
|
| 297 |
-
fn=handoff_to_human,
|
| 298 |
-
inputs=[session_id_input],
|
| 299 |
-
outputs=[action_output]
|
| 300 |
-
)
|
| 301 |
-
|
| 302 |
-
close_session_btn.click(
|
| 303 |
-
fn=close_chat_session,
|
| 304 |
-
inputs=[session_id_input],
|
| 305 |
-
outputs=[action_output]
|
| 306 |
-
)
|
| 307 |
-
|
| 308 |
-
test_bot_btn.click(
|
| 309 |
-
fn=test_bot_response,
|
| 310 |
-
inputs=[test_message_input],
|
| 311 |
-
outputs=[bot_response_output, bot_metadata_output]
|
| 312 |
-
)
|
| 313 |
-
|
| 314 |
-
refresh_stats_btn.click(
|
| 315 |
-
fn=load_chat_stats,
|
| 316 |
-
outputs=[total_sessions_metric, avg_rating_metric, handoff_rate_metric, chat_stats_json]
|
| 317 |
-
)
|
| 318 |
-
|
| 319 |
-
# Load initial data
|
| 320 |
-
chat_interface.load(
|
| 321 |
-
fn=load_active_sessions,
|
| 322 |
-
outputs=[active_sessions_table, active_count, waiting_count, avg_wait_time]
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
return chat_interface
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
def render_chat_ui() -> gr.Blocks:
|
| 329 |
-
"""Convenience function to render chat UI"""
|
| 330 |
-
ui = ChatUI()
|
| 331 |
-
return ui.render()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Knowledge Base Module
|
| 3 |
-
Handles articles, semantic search with RAG, and AI content generation
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from .manager import KnowledgeBaseManager
|
| 7 |
-
from .search import SemanticSearchEngine
|
| 8 |
-
from .ui import render_kb_ui
|
| 9 |
-
|
| 10 |
-
__all__ = ['KnowledgeBaseManager', 'SemanticSearchEngine', 'render_kb_ui']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,475 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Knowledge Base Manager - CRUD operations for articles and categories
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
from typing import Optional, List, Dict, Any, Tuple
|
| 6 |
-
from datetime import datetime
|
| 7 |
-
from sqlalchemy.orm import Session
|
| 8 |
-
from sqlalchemy import desc, or_, func
|
| 9 |
-
import json
|
| 10 |
-
import re
|
| 11 |
-
|
| 12 |
-
from models.cx_models import CXKBArticle, CXKBCategory, CXKBArticleVersion
|
| 13 |
-
from database.manager import get_db_manager
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
class KnowledgeBaseManager:
|
| 17 |
-
"""Manages knowledge base articles and categories"""
|
| 18 |
-
|
| 19 |
-
def __init__(self):
|
| 20 |
-
self.db = get_db_manager()
|
| 21 |
-
|
| 22 |
-
# =============================================================================
|
| 23 |
-
# CATEGORY MANAGEMENT
|
| 24 |
-
# =============================================================================
|
| 25 |
-
|
| 26 |
-
def create_category(
|
| 27 |
-
self,
|
| 28 |
-
name: str,
|
| 29 |
-
description: Optional[str] = None,
|
| 30 |
-
parent_id: Optional[int] = None,
|
| 31 |
-
icon: Optional[str] = None
|
| 32 |
-
) -> CXKBCategory:
|
| 33 |
-
"""Create a new KB category"""
|
| 34 |
-
with self.db.get_session() as session:
|
| 35 |
-
category = CXKBCategory(
|
| 36 |
-
name=name,
|
| 37 |
-
description=description,
|
| 38 |
-
parent_id=parent_id,
|
| 39 |
-
icon=icon
|
| 40 |
-
)
|
| 41 |
-
session.add(category)
|
| 42 |
-
session.commit()
|
| 43 |
-
session.refresh(category)
|
| 44 |
-
return category
|
| 45 |
-
|
| 46 |
-
def get_all_categories(self, active_only: bool = True) -> List[CXKBCategory]:
|
| 47 |
-
"""Get all categories"""
|
| 48 |
-
with self.db.get_session() as session:
|
| 49 |
-
query = session.query(CXKBCategory)
|
| 50 |
-
if active_only:
|
| 51 |
-
query = query.filter(CXKBCategory.is_active == True)
|
| 52 |
-
|
| 53 |
-
categories = query.order_by(CXKBCategory.display_order).all()
|
| 54 |
-
return categories
|
| 55 |
-
|
| 56 |
-
def get_category(self, category_id: int) -> Optional[CXKBCategory]:
|
| 57 |
-
"""Get category by ID"""
|
| 58 |
-
with self.db.get_session() as session:
|
| 59 |
-
return session.query(CXKBCategory).filter_by(id=category_id).first()
|
| 60 |
-
|
| 61 |
-
# =============================================================================
|
| 62 |
-
# ARTICLE CRUD
|
| 63 |
-
# =============================================================================
|
| 64 |
-
|
| 65 |
-
def create_article(
|
| 66 |
-
self,
|
| 67 |
-
title: str,
|
| 68 |
-
content: str,
|
| 69 |
-
category_id: Optional[int] = None,
|
| 70 |
-
summary: Optional[str] = None,
|
| 71 |
-
author: Optional[str] = None,
|
| 72 |
-
tags: Optional[List[str]] = None,
|
| 73 |
-
status: str = 'draft',
|
| 74 |
-
visibility: str = 'public',
|
| 75 |
-
ai_generated: bool = False
|
| 76 |
-
) -> CXKBArticle:
|
| 77 |
-
"""
|
| 78 |
-
Create a new KB article
|
| 79 |
-
|
| 80 |
-
Args:
|
| 81 |
-
title: Article title
|
| 82 |
-
content: Article content (markdown)
|
| 83 |
-
category_id: Optional category ID
|
| 84 |
-
summary: Brief summary
|
| 85 |
-
author: Author name
|
| 86 |
-
tags: List of tags
|
| 87 |
-
status: draft, published, archived
|
| 88 |
-
visibility: public, internal, private
|
| 89 |
-
ai_generated: Whether content was AI-generated
|
| 90 |
-
"""
|
| 91 |
-
with self.db.get_session() as session:
|
| 92 |
-
# Generate slug from title
|
| 93 |
-
slug = self._generate_slug(title)
|
| 94 |
-
|
| 95 |
-
# Check for duplicate slug
|
| 96 |
-
existing = session.query(CXKBArticle).filter_by(slug=slug).first()
|
| 97 |
-
if existing:
|
| 98 |
-
slug = f"{slug}-{datetime.utcnow().timestamp()}"
|
| 99 |
-
|
| 100 |
-
article = CXKBArticle(
|
| 101 |
-
title=title,
|
| 102 |
-
content=content,
|
| 103 |
-
category_id=category_id,
|
| 104 |
-
summary=summary,
|
| 105 |
-
author=author,
|
| 106 |
-
slug=slug,
|
| 107 |
-
status=status,
|
| 108 |
-
visibility=visibility,
|
| 109 |
-
ai_generated=ai_generated
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
if tags:
|
| 113 |
-
article.tags = json.dumps(tags)
|
| 114 |
-
|
| 115 |
-
session.add(article)
|
| 116 |
-
session.flush()
|
| 117 |
-
|
| 118 |
-
# Create initial version
|
| 119 |
-
self._create_version(session, article, author, "Initial version")
|
| 120 |
-
|
| 121 |
-
# Auto-publish if requested
|
| 122 |
-
if status == 'published':
|
| 123 |
-
article.published_at = datetime.utcnow()
|
| 124 |
-
|
| 125 |
-
session.commit()
|
| 126 |
-
session.refresh(article)
|
| 127 |
-
|
| 128 |
-
return article
|
| 129 |
-
|
| 130 |
-
def get_article(self, article_id: int) -> Optional[CXKBArticle]:
|
| 131 |
-
"""Get article by ID"""
|
| 132 |
-
with self.db.get_session() as session:
|
| 133 |
-
article = session.query(CXKBArticle).filter_by(id=article_id).first()
|
| 134 |
-
if article:
|
| 135 |
-
_ = article.category # Eager load
|
| 136 |
-
return article
|
| 137 |
-
|
| 138 |
-
def get_article_by_slug(self, slug: str) -> Optional[CXKBArticle]:
|
| 139 |
-
"""Get article by slug"""
|
| 140 |
-
with self.db.get_session() as session:
|
| 141 |
-
return session.query(CXKBArticle).filter_by(slug=slug).first()
|
| 142 |
-
|
| 143 |
-
def get_all_articles(
|
| 144 |
-
self,
|
| 145 |
-
status: Optional[str] = None,
|
| 146 |
-
category_id: Optional[int] = None,
|
| 147 |
-
visibility: Optional[str] = None,
|
| 148 |
-
limit: int = 100,
|
| 149 |
-
offset: int = 0
|
| 150 |
-
) -> Tuple[List[CXKBArticle], int]:
|
| 151 |
-
"""
|
| 152 |
-
Get articles with filters
|
| 153 |
-
|
| 154 |
-
Returns:
|
| 155 |
-
Tuple of (articles list, total count)
|
| 156 |
-
"""
|
| 157 |
-
with self.db.get_session() as session:
|
| 158 |
-
query = session.query(CXKBArticle)
|
| 159 |
-
|
| 160 |
-
# Apply filters
|
| 161 |
-
if status:
|
| 162 |
-
query = query.filter(CXKBArticle.status == status)
|
| 163 |
-
if category_id:
|
| 164 |
-
query = query.filter(CXKBArticle.category_id == category_id)
|
| 165 |
-
if visibility:
|
| 166 |
-
query = query.filter(CXKBArticle.visibility == visibility)
|
| 167 |
-
|
| 168 |
-
# Get total count
|
| 169 |
-
total = query.count()
|
| 170 |
-
|
| 171 |
-
# Apply pagination
|
| 172 |
-
articles = query.order_by(desc(CXKBArticle.created_at)).limit(limit).offset(offset).all()
|
| 173 |
-
|
| 174 |
-
# Eager load categories
|
| 175 |
-
for article in articles:
|
| 176 |
-
_ = article.category
|
| 177 |
-
|
| 178 |
-
return articles, total
|
| 179 |
-
|
| 180 |
-
def update_article(
|
| 181 |
-
self,
|
| 182 |
-
article_id: int,
|
| 183 |
-
changed_by: Optional[str] = None,
|
| 184 |
-
change_note: Optional[str] = None,
|
| 185 |
-
**updates
|
| 186 |
-
) -> CXKBArticle:
|
| 187 |
-
"""
|
| 188 |
-
Update article
|
| 189 |
-
|
| 190 |
-
Args:
|
| 191 |
-
article_id: Article ID
|
| 192 |
-
changed_by: Who made the change
|
| 193 |
-
change_note: Reason for change
|
| 194 |
-
**updates: Fields to update
|
| 195 |
-
"""
|
| 196 |
-
with self.db.get_session() as session:
|
| 197 |
-
article = session.query(CXKBArticle).filter_by(id=article_id).first()
|
| 198 |
-
if not article:
|
| 199 |
-
raise ValueError(f"Article {article_id} not found")
|
| 200 |
-
|
| 201 |
-
# Track if content changed (triggers new version)
|
| 202 |
-
content_changed = 'content' in updates or 'title' in updates
|
| 203 |
-
|
| 204 |
-
# Update fields
|
| 205 |
-
for key, value in updates.items():
|
| 206 |
-
if hasattr(article, key):
|
| 207 |
-
setattr(article, key, value)
|
| 208 |
-
|
| 209 |
-
# Handle status changes
|
| 210 |
-
if 'status' in updates:
|
| 211 |
-
if updates['status'] == 'published' and not article.published_at:
|
| 212 |
-
article.published_at = datetime.utcnow()
|
| 213 |
-
|
| 214 |
-
# Create new version if content changed
|
| 215 |
-
if content_changed:
|
| 216 |
-
article.version += 1
|
| 217 |
-
self._create_version(session, article, changed_by, change_note)
|
| 218 |
-
|
| 219 |
-
session.commit()
|
| 220 |
-
session.refresh(article)
|
| 221 |
-
|
| 222 |
-
return article
|
| 223 |
-
|
| 224 |
-
def delete_article(self, article_id: int) -> bool:
|
| 225 |
-
"""Delete article (soft delete - set to archived)"""
|
| 226 |
-
with self.db.get_session() as session:
|
| 227 |
-
article = session.query(CXKBArticle).filter_by(id=article_id).first()
|
| 228 |
-
if not article:
|
| 229 |
-
return False
|
| 230 |
-
|
| 231 |
-
article.status = 'archived'
|
| 232 |
-
session.commit()
|
| 233 |
-
return True
|
| 234 |
-
|
| 235 |
-
# =============================================================================
|
| 236 |
-
# ARTICLE VERSIONING
|
| 237 |
-
# =============================================================================
|
| 238 |
-
|
| 239 |
-
def _create_version(
|
| 240 |
-
self,
|
| 241 |
-
session: Session,
|
| 242 |
-
article: CXKBArticle,
|
| 243 |
-
changed_by: Optional[str],
|
| 244 |
-
change_note: Optional[str]
|
| 245 |
-
) -> CXKBArticleVersion:
|
| 246 |
-
"""Create a version snapshot"""
|
| 247 |
-
version = CXKBArticleVersion(
|
| 248 |
-
article_id=article.id,
|
| 249 |
-
version=article.version,
|
| 250 |
-
title=article.title,
|
| 251 |
-
content=article.content,
|
| 252 |
-
changed_by=changed_by,
|
| 253 |
-
change_note=change_note
|
| 254 |
-
)
|
| 255 |
-
session.add(version)
|
| 256 |
-
return version
|
| 257 |
-
|
| 258 |
-
def get_article_versions(self, article_id: int) -> List[CXKBArticleVersion]:
|
| 259 |
-
"""Get all versions of an article"""
|
| 260 |
-
with self.db.get_session() as session:
|
| 261 |
-
versions = session.query(CXKBArticleVersion).filter(
|
| 262 |
-
CXKBArticleVersion.article_id == article_id
|
| 263 |
-
).order_by(desc(CXKBArticleVersion.version)).all()
|
| 264 |
-
return versions
|
| 265 |
-
|
| 266 |
-
def restore_version(
|
| 267 |
-
self,
|
| 268 |
-
article_id: int,
|
| 269 |
-
version_number: int,
|
| 270 |
-
restored_by: Optional[str] = None
|
| 271 |
-
) -> CXKBArticle:
|
| 272 |
-
"""Restore article to a previous version"""
|
| 273 |
-
with self.db.get_session() as session:
|
| 274 |
-
article = session.query(CXKBArticle).filter_by(id=article_id).first()
|
| 275 |
-
if not article:
|
| 276 |
-
raise ValueError(f"Article {article_id} not found")
|
| 277 |
-
|
| 278 |
-
version = session.query(CXKBArticleVersion).filter_by(
|
| 279 |
-
article_id=article_id,
|
| 280 |
-
version=version_number
|
| 281 |
-
).first()
|
| 282 |
-
|
| 283 |
-
if not version:
|
| 284 |
-
raise ValueError(f"Version {version_number} not found")
|
| 285 |
-
|
| 286 |
-
# Restore content
|
| 287 |
-
article.title = version.title
|
| 288 |
-
article.content = version.content
|
| 289 |
-
article.version += 1
|
| 290 |
-
|
| 291 |
-
# Create new version record
|
| 292 |
-
self._create_version(
|
| 293 |
-
session,
|
| 294 |
-
article,
|
| 295 |
-
restored_by,
|
| 296 |
-
f"Restored from version {version_number}"
|
| 297 |
-
)
|
| 298 |
-
|
| 299 |
-
session.commit()
|
| 300 |
-
session.refresh(article)
|
| 301 |
-
|
| 302 |
-
return article
|
| 303 |
-
|
| 304 |
-
# =============================================================================
|
| 305 |
-
# SEARCH & DISCOVERY
|
| 306 |
-
# =============================================================================
|
| 307 |
-
|
| 308 |
-
def search_articles(
|
| 309 |
-
self,
|
| 310 |
-
query: str,
|
| 311 |
-
status: str = 'published',
|
| 312 |
-
limit: int = 20
|
| 313 |
-
) -> List[CXKBArticle]:
|
| 314 |
-
"""
|
| 315 |
-
Simple keyword search in articles
|
| 316 |
-
|
| 317 |
-
For semantic search, use SemanticSearchEngine
|
| 318 |
-
"""
|
| 319 |
-
with self.db.get_session() as session:
|
| 320 |
-
search_term = f"%{query}%"
|
| 321 |
-
|
| 322 |
-
articles = session.query(CXKBArticle).filter(
|
| 323 |
-
CXKBArticle.status == status,
|
| 324 |
-
or_(
|
| 325 |
-
CXKBArticle.title.like(search_term),
|
| 326 |
-
CXKBArticle.content.like(search_term),
|
| 327 |
-
CXKBArticle.summary.like(search_term)
|
| 328 |
-
)
|
| 329 |
-
).limit(limit).all()
|
| 330 |
-
|
| 331 |
-
return articles
|
| 332 |
-
|
| 333 |
-
def get_popular_articles(
|
| 334 |
-
self,
|
| 335 |
-
limit: int = 10,
|
| 336 |
-
days: int = 30
|
| 337 |
-
) -> List[CXKBArticle]:
|
| 338 |
-
"""Get most viewed articles"""
|
| 339 |
-
with self.db.get_session() as session:
|
| 340 |
-
articles = session.query(CXKBArticle).filter(
|
| 341 |
-
CXKBArticle.status == 'published'
|
| 342 |
-
).order_by(desc(CXKBArticle.view_count)).limit(limit).all()
|
| 343 |
-
|
| 344 |
-
return articles
|
| 345 |
-
|
| 346 |
-
def get_helpful_articles(self, limit: int = 10) -> List[CXKBArticle]:
|
| 347 |
-
"""Get most helpful articles (by vote ratio)"""
|
| 348 |
-
with self.db.get_session() as session:
|
| 349 |
-
articles = session.query(CXKBArticle).filter(
|
| 350 |
-
CXKBArticle.status == 'published',
|
| 351 |
-
CXKBArticle.helpful_count > 0
|
| 352 |
-
).all()
|
| 353 |
-
|
| 354 |
-
# Calculate helpfulness ratio
|
| 355 |
-
scored_articles = []
|
| 356 |
-
for article in articles:
|
| 357 |
-
total = article.helpful_count + article.not_helpful_count
|
| 358 |
-
if total > 0:
|
| 359 |
-
ratio = article.helpful_count / total
|
| 360 |
-
scored_articles.append((article, ratio))
|
| 361 |
-
|
| 362 |
-
# Sort by ratio
|
| 363 |
-
scored_articles.sort(key=lambda x: x[1], reverse=True)
|
| 364 |
-
|
| 365 |
-
return [article for article, _ in scored_articles[:limit]]
|
| 366 |
-
|
| 367 |
-
# =============================================================================
|
| 368 |
-
# ARTICLE METRICS
|
| 369 |
-
# =============================================================================
|
| 370 |
-
|
| 371 |
-
def track_view(self, article_id: int):
|
| 372 |
-
"""Increment article view count"""
|
| 373 |
-
with self.db.get_session() as session:
|
| 374 |
-
article = session.query(CXKBArticle).filter_by(id=article_id).first()
|
| 375 |
-
if article:
|
| 376 |
-
article.view_count += 1
|
| 377 |
-
session.commit()
|
| 378 |
-
|
| 379 |
-
def vote_helpful(self, article_id: int, is_helpful: bool):
|
| 380 |
-
"""Record helpfulness vote"""
|
| 381 |
-
with self.db.get_session() as session:
|
| 382 |
-
article = session.query(CXKBArticle).filter_by(id=article_id).first()
|
| 383 |
-
if not article:
|
| 384 |
-
return
|
| 385 |
-
|
| 386 |
-
if is_helpful:
|
| 387 |
-
article.helpful_count += 1
|
| 388 |
-
else:
|
| 389 |
-
article.not_helpful_count += 1
|
| 390 |
-
|
| 391 |
-
# Recalculate average rating
|
| 392 |
-
total = article.helpful_count + article.not_helpful_count
|
| 393 |
-
if total > 0:
|
| 394 |
-
article.average_rating = article.helpful_count / total
|
| 395 |
-
|
| 396 |
-
session.commit()
|
| 397 |
-
|
| 398 |
-
# =============================================================================
|
| 399 |
-
# AI CONTENT GENERATION
|
| 400 |
-
# =============================================================================
|
| 401 |
-
|
| 402 |
-
def generate_article_from_tickets(
|
| 403 |
-
self,
|
| 404 |
-
ticket_ids: List[int],
|
| 405 |
-
author: str = "AI Assistant"
|
| 406 |
-
) -> CXKBArticle:
|
| 407 |
-
"""
|
| 408 |
-
Generate a KB article from common ticket patterns
|
| 409 |
-
|
| 410 |
-
TODO: Implement with AI agent
|
| 411 |
-
"""
|
| 412 |
-
# Placeholder for AI generation
|
| 413 |
-
title = "Auto-Generated Article"
|
| 414 |
-
content = "Content generated from ticket analysis..."
|
| 415 |
-
|
| 416 |
-
return self.create_article(
|
| 417 |
-
title=title,
|
| 418 |
-
content=content,
|
| 419 |
-
author=author,
|
| 420 |
-
ai_generated=True,
|
| 421 |
-
status='draft'
|
| 422 |
-
)
|
| 423 |
-
|
| 424 |
-
def suggest_improvements(self, article_id: int) -> Dict[str, Any]:
|
| 425 |
-
"""
|
| 426 |
-
Suggest improvements for an article using AI
|
| 427 |
-
|
| 428 |
-
TODO: Implement with AI agent
|
| 429 |
-
|
| 430 |
-
Returns:
|
| 431 |
-
Dict with suggestions
|
| 432 |
-
"""
|
| 433 |
-
return {
|
| 434 |
-
'clarity_score': 0.85,
|
| 435 |
-
'completeness_score': 0.75,
|
| 436 |
-
'suggestions': [
|
| 437 |
-
"Add more examples",
|
| 438 |
-
"Improve formatting",
|
| 439 |
-
"Add troubleshooting section"
|
| 440 |
-
]
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
# =============================================================================
|
| 444 |
-
# UTILITIES
|
| 445 |
-
# =============================================================================
|
| 446 |
-
|
| 447 |
-
def _generate_slug(self, title: str) -> str:
|
| 448 |
-
"""Generate URL-friendly slug from title"""
|
| 449 |
-
# Convert to lowercase
|
| 450 |
-
slug = title.lower()
|
| 451 |
-
|
| 452 |
-
# Replace spaces and special chars with hyphens
|
| 453 |
-
slug = re.sub(r'[^\w\s-]', '', slug)
|
| 454 |
-
slug = re.sub(r'[\s_]+', '-', slug)
|
| 455 |
-
slug = slug.strip('-')
|
| 456 |
-
|
| 457 |
-
return slug[:100] # Limit length
|
| 458 |
-
|
| 459 |
-
def get_kb_stats(self) -> Dict[str, Any]:
|
| 460 |
-
"""Get overall KB statistics"""
|
| 461 |
-
with self.db.get_session() as session:
|
| 462 |
-
total_articles = session.query(func.count(CXKBArticle.id)).scalar()
|
| 463 |
-
published_articles = session.query(func.count(CXKBArticle.id)).filter(
|
| 464 |
-
CXKBArticle.status == 'published'
|
| 465 |
-
).scalar()
|
| 466 |
-
total_views = session.query(func.sum(CXKBArticle.view_count)).scalar() or 0
|
| 467 |
-
total_categories = session.query(func.count(CXKBCategory.id)).scalar()
|
| 468 |
-
|
| 469 |
-
return {
|
| 470 |
-
'total_articles': total_articles,
|
| 471 |
-
'published_articles': published_articles,
|
| 472 |
-
'draft_articles': total_articles - published_articles,
|
| 473 |
-
'total_views': total_views,
|
| 474 |
-
'total_categories': total_categories
|
| 475 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,361 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Semantic Search Engine - RAG-powered knowledge base search
|
| 3 |
-
Uses sentence transformers and FAISS for vector similarity search
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import List, Dict, Any, Tuple, Optional
|
| 7 |
-
import os
|
| 8 |
-
import pickle
|
| 9 |
-
import numpy as np
|
| 10 |
-
from pathlib import Path
|
| 11 |
-
|
| 12 |
-
try:
|
| 13 |
-
import faiss
|
| 14 |
-
from sentence_transformers import SentenceTransformer
|
| 15 |
-
SEMANTIC_SEARCH_AVAILABLE = True
|
| 16 |
-
except ImportError:
|
| 17 |
-
SEMANTIC_SEARCH_AVAILABLE = False
|
| 18 |
-
|
| 19 |
-
from models.cx_models import CXKBArticle
|
| 20 |
-
from .manager import KnowledgeBaseManager
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
class SemanticSearchEngine:
|
| 24 |
-
"""
|
| 25 |
-
Semantic search engine for KB articles using RAG
|
| 26 |
-
|
| 27 |
-
Features:
|
| 28 |
-
- Vector embeddings with sentence-transformers
|
| 29 |
-
- FAISS vector similarity search
|
| 30 |
-
- Automatic index building and updates
|
| 31 |
-
- Hybrid search (keyword + semantic)
|
| 32 |
-
"""
|
| 33 |
-
|
| 34 |
-
def __init__(
|
| 35 |
-
self,
|
| 36 |
-
model_name: str = 'all-MiniLM-L6-v2',
|
| 37 |
-
index_path: str = './data/kb_index'
|
| 38 |
-
):
|
| 39 |
-
"""
|
| 40 |
-
Initialize semantic search engine
|
| 41 |
-
|
| 42 |
-
Args:
|
| 43 |
-
model_name: HuggingFace sentence-transformer model
|
| 44 |
-
index_path: Path to store FAISS index
|
| 45 |
-
"""
|
| 46 |
-
self.kb_manager = KnowledgeBaseManager()
|
| 47 |
-
self.index_path = Path(index_path)
|
| 48 |
-
self.index_path.mkdir(parents=True, exist_ok=True)
|
| 49 |
-
|
| 50 |
-
# Check if dependencies available
|
| 51 |
-
if not SEMANTIC_SEARCH_AVAILABLE:
|
| 52 |
-
print("⚠️ Warning: sentence-transformers or faiss-cpu not installed. Semantic search disabled.")
|
| 53 |
-
self.enabled = False
|
| 54 |
-
return
|
| 55 |
-
|
| 56 |
-
self.enabled = True
|
| 57 |
-
|
| 58 |
-
# Load embedding model
|
| 59 |
-
print(f"Loading embedding model: {model_name}...")
|
| 60 |
-
self.model = SentenceTransformer(model_name)
|
| 61 |
-
self.embedding_dim = self.model.get_sentence_embedding_dimension()
|
| 62 |
-
|
| 63 |
-
# Load or create FAISS index
|
| 64 |
-
self.index = None
|
| 65 |
-
self.article_ids = []
|
| 66 |
-
self.load_index()
|
| 67 |
-
|
| 68 |
-
# =============================================================================
|
| 69 |
-
# INDEX MANAGEMENT
|
| 70 |
-
# =============================================================================
|
| 71 |
-
|
| 72 |
-
def build_index(self, force_rebuild: bool = False):
|
| 73 |
-
"""
|
| 74 |
-
Build FAISS index from all published articles
|
| 75 |
-
|
| 76 |
-
Args:
|
| 77 |
-
force_rebuild: Rebuild even if index exists
|
| 78 |
-
"""
|
| 79 |
-
if not self.enabled:
|
| 80 |
-
return
|
| 81 |
-
|
| 82 |
-
index_file = self.index_path / 'faiss.index'
|
| 83 |
-
mapping_file = self.index_path / 'article_mapping.pkl'
|
| 84 |
-
|
| 85 |
-
# Check if we need to rebuild
|
| 86 |
-
if not force_rebuild and index_file.exists() and mapping_file.exists():
|
| 87 |
-
print("Index already exists. Use force_rebuild=True to rebuild.")
|
| 88 |
-
return
|
| 89 |
-
|
| 90 |
-
print("Building semantic search index...")
|
| 91 |
-
|
| 92 |
-
# Get all published articles
|
| 93 |
-
articles, _ = self.kb_manager.get_all_articles(status='published', limit=10000)
|
| 94 |
-
|
| 95 |
-
if not articles:
|
| 96 |
-
print("No published articles found. Skipping index build.")
|
| 97 |
-
return
|
| 98 |
-
|
| 99 |
-
# Prepare article texts for embedding
|
| 100 |
-
article_texts = []
|
| 101 |
-
article_ids = []
|
| 102 |
-
|
| 103 |
-
for article in articles:
|
| 104 |
-
# Combine title, summary, and content for better embeddings
|
| 105 |
-
text = f"{article.title}. {article.summary or ''}. {article.content[:500]}"
|
| 106 |
-
article_texts.append(text)
|
| 107 |
-
article_ids.append(article.id)
|
| 108 |
-
|
| 109 |
-
# Generate embeddings
|
| 110 |
-
print(f"Generating embeddings for {len(article_texts)} articles...")
|
| 111 |
-
embeddings = self.model.encode(
|
| 112 |
-
article_texts,
|
| 113 |
-
convert_to_numpy=True,
|
| 114 |
-
show_progress_bar=True,
|
| 115 |
-
batch_size=32
|
| 116 |
-
)
|
| 117 |
-
|
| 118 |
-
# Create FAISS index
|
| 119 |
-
print("Creating FAISS index...")
|
| 120 |
-
index = faiss.IndexFlatL2(self.embedding_dim) # L2 distance
|
| 121 |
-
index.add(embeddings.astype('float32'))
|
| 122 |
-
|
| 123 |
-
# Save index and mapping
|
| 124 |
-
faiss.write_index(index, str(index_file))
|
| 125 |
-
|
| 126 |
-
with open(mapping_file, 'wb') as f:
|
| 127 |
-
pickle.dump(article_ids, f)
|
| 128 |
-
|
| 129 |
-
self.index = index
|
| 130 |
-
self.article_ids = article_ids
|
| 131 |
-
|
| 132 |
-
print(f"✅ Index built successfully: {len(article_ids)} articles indexed")
|
| 133 |
-
|
| 134 |
-
def load_index(self):
|
| 135 |
-
"""Load existing FAISS index"""
|
| 136 |
-
if not self.enabled:
|
| 137 |
-
return
|
| 138 |
-
|
| 139 |
-
index_file = self.index_path / 'faiss.index'
|
| 140 |
-
mapping_file = self.index_path / 'article_mapping.pkl'
|
| 141 |
-
|
| 142 |
-
if not index_file.exists() or not mapping_file.exists():
|
| 143 |
-
print("No existing index found. Run build_index() to create one.")
|
| 144 |
-
return
|
| 145 |
-
|
| 146 |
-
try:
|
| 147 |
-
self.index = faiss.read_index(str(index_file))
|
| 148 |
-
|
| 149 |
-
with open(mapping_file, 'rb') as f:
|
| 150 |
-
self.article_ids = pickle.load(f)
|
| 151 |
-
|
| 152 |
-
print(f"✅ Index loaded: {len(self.article_ids)} articles")
|
| 153 |
-
|
| 154 |
-
except Exception as e:
|
| 155 |
-
print(f"Error loading index: {e}")
|
| 156 |
-
self.index = None
|
| 157 |
-
self.article_ids = []
|
| 158 |
-
|
| 159 |
-
def update_index_for_article(self, article_id: int):
|
| 160 |
-
"""
|
| 161 |
-
Update index when an article is modified
|
| 162 |
-
|
| 163 |
-
For simplicity, we rebuild the entire index.
|
| 164 |
-
In production, you'd want incremental updates.
|
| 165 |
-
"""
|
| 166 |
-
if not self.enabled:
|
| 167 |
-
return
|
| 168 |
-
|
| 169 |
-
print("Rebuilding index after article update...")
|
| 170 |
-
self.build_index(force_rebuild=True)
|
| 171 |
-
|
| 172 |
-
# =============================================================================
|
| 173 |
-
# SEARCH
|
| 174 |
-
# =============================================================================
|
| 175 |
-
|
| 176 |
-
def search(
|
| 177 |
-
self,
|
| 178 |
-
query: str,
|
| 179 |
-
top_k: int = 5,
|
| 180 |
-
threshold: float = 0.5
|
| 181 |
-
) -> List[Tuple[CXKBArticle, float]]:
|
| 182 |
-
"""
|
| 183 |
-
Semantic search for articles
|
| 184 |
-
|
| 185 |
-
Args:
|
| 186 |
-
query: Search query
|
| 187 |
-
top_k: Number of results to return
|
| 188 |
-
threshold: Minimum similarity score (0-1)
|
| 189 |
-
|
| 190 |
-
Returns:
|
| 191 |
-
List of (article, similarity_score) tuples
|
| 192 |
-
"""
|
| 193 |
-
if not self.enabled or self.index is None:
|
| 194 |
-
# Fallback to keyword search
|
| 195 |
-
print("Semantic search not available, using keyword search")
|
| 196 |
-
articles = self.kb_manager.search_articles(query, limit=top_k)
|
| 197 |
-
return [(article, 0.5) for article in articles]
|
| 198 |
-
|
| 199 |
-
# Generate query embedding
|
| 200 |
-
query_embedding = self.model.encode([query], convert_to_numpy=True)
|
| 201 |
-
|
| 202 |
-
# Search FAISS index
|
| 203 |
-
distances, indices = self.index.search(query_embedding.astype('float32'), top_k)
|
| 204 |
-
|
| 205 |
-
# Convert distances to similarity scores (0-1)
|
| 206 |
-
# Using exponential decay: similarity = exp(-distance)
|
| 207 |
-
similarities = np.exp(-distances[0])
|
| 208 |
-
|
| 209 |
-
# Get articles
|
| 210 |
-
results = []
|
| 211 |
-
for idx, similarity in zip(indices[0], similarities):
|
| 212 |
-
if similarity < threshold:
|
| 213 |
-
continue
|
| 214 |
-
|
| 215 |
-
article_id = self.article_ids[idx]
|
| 216 |
-
article = self.kb_manager.get_article(article_id)
|
| 217 |
-
|
| 218 |
-
if article:
|
| 219 |
-
results.append((article, float(similarity)))
|
| 220 |
-
|
| 221 |
-
return results
|
| 222 |
-
|
| 223 |
-
def hybrid_search(
|
| 224 |
-
self,
|
| 225 |
-
query: str,
|
| 226 |
-
top_k: int = 5,
|
| 227 |
-
semantic_weight: float = 0.7
|
| 228 |
-
) -> List[Tuple[CXKBArticle, float]]:
|
| 229 |
-
"""
|
| 230 |
-
Hybrid search combining semantic and keyword search
|
| 231 |
-
|
| 232 |
-
Args:
|
| 233 |
-
query: Search query
|
| 234 |
-
top_k: Number of results to return
|
| 235 |
-
semantic_weight: Weight for semantic score (0-1), keyword = 1 - semantic_weight
|
| 236 |
-
|
| 237 |
-
Returns:
|
| 238 |
-
List of (article, combined_score) tuples
|
| 239 |
-
"""
|
| 240 |
-
# Get semantic results
|
| 241 |
-
semantic_results = self.search(query, top_k=top_k * 2)
|
| 242 |
-
|
| 243 |
-
# Get keyword results
|
| 244 |
-
keyword_articles = self.kb_manager.search_articles(query, limit=top_k * 2)
|
| 245 |
-
|
| 246 |
-
# Combine scores
|
| 247 |
-
combined_scores = {}
|
| 248 |
-
|
| 249 |
-
# Add semantic scores
|
| 250 |
-
for article, score in semantic_results:
|
| 251 |
-
combined_scores[article.id] = {
|
| 252 |
-
'article': article,
|
| 253 |
-
'semantic_score': score,
|
| 254 |
-
'keyword_score': 0.0
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
# Add keyword scores (simple presence = 0.5)
|
| 258 |
-
for article in keyword_articles:
|
| 259 |
-
if article.id in combined_scores:
|
| 260 |
-
combined_scores[article.id]['keyword_score'] = 0.5
|
| 261 |
-
else:
|
| 262 |
-
combined_scores[article.id] = {
|
| 263 |
-
'article': article,
|
| 264 |
-
'semantic_score': 0.0,
|
| 265 |
-
'keyword_score': 0.5
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
# Calculate combined scores
|
| 269 |
-
results = []
|
| 270 |
-
for article_id, scores in combined_scores.items():
|
| 271 |
-
combined = (
|
| 272 |
-
scores['semantic_score'] * semantic_weight +
|
| 273 |
-
scores['keyword_score'] * (1 - semantic_weight)
|
| 274 |
-
)
|
| 275 |
-
results.append((scores['article'], combined))
|
| 276 |
-
|
| 277 |
-
# Sort by combined score
|
| 278 |
-
results.sort(key=lambda x: x[1], reverse=True)
|
| 279 |
-
|
| 280 |
-
return results[:top_k]
|
| 281 |
-
|
| 282 |
-
# =============================================================================
|
| 283 |
-
# RAG - RETRIEVAL AUGMENTED GENERATION
|
| 284 |
-
# =============================================================================
|
| 285 |
-
|
| 286 |
-
def get_relevant_context(
|
| 287 |
-
self,
|
| 288 |
-
query: str,
|
| 289 |
-
max_articles: int = 3,
|
| 290 |
-
max_chars: int = 2000
|
| 291 |
-
) -> str:
|
| 292 |
-
"""
|
| 293 |
-
Get relevant article content as context for AI generation
|
| 294 |
-
|
| 295 |
-
Args:
|
| 296 |
-
query: User query
|
| 297 |
-
max_articles: Maximum number of articles to include
|
| 298 |
-
max_chars: Maximum total characters
|
| 299 |
-
|
| 300 |
-
Returns:
|
| 301 |
-
Concatenated relevant article content
|
| 302 |
-
"""
|
| 303 |
-
results = self.search(query, top_k=max_articles)
|
| 304 |
-
|
| 305 |
-
context_parts = []
|
| 306 |
-
total_chars = 0
|
| 307 |
-
|
| 308 |
-
for article, score in results:
|
| 309 |
-
# Build context snippet
|
| 310 |
-
snippet = f"**{article.title}**\n{article.content}\n\n"
|
| 311 |
-
|
| 312 |
-
# Check if we're over limit
|
| 313 |
-
if total_chars + len(snippet) > max_chars:
|
| 314 |
-
# Truncate last snippet
|
| 315 |
-
remaining = max_chars - total_chars
|
| 316 |
-
if remaining > 100: # Only add if meaningful
|
| 317 |
-
snippet = snippet[:remaining] + "..."
|
| 318 |
-
context_parts.append(snippet)
|
| 319 |
-
break
|
| 320 |
-
|
| 321 |
-
context_parts.append(snippet)
|
| 322 |
-
total_chars += len(snippet)
|
| 323 |
-
|
| 324 |
-
return "\n".join(context_parts)
|
| 325 |
-
|
| 326 |
-
def suggest_articles_for_ticket(
|
| 327 |
-
self,
|
| 328 |
-
ticket_description: str,
|
| 329 |
-
top_k: int = 3
|
| 330 |
-
) -> List[Tuple[CXKBArticle, float]]:
|
| 331 |
-
"""
|
| 332 |
-
Suggest relevant KB articles for a support ticket
|
| 333 |
-
|
| 334 |
-
Args:
|
| 335 |
-
ticket_description: Ticket description
|
| 336 |
-
top_k: Number of suggestions
|
| 337 |
-
|
| 338 |
-
Returns:
|
| 339 |
-
List of (article, relevance_score) tuples
|
| 340 |
-
"""
|
| 341 |
-
return self.search(ticket_description, top_k=top_k)
|
| 342 |
-
|
| 343 |
-
# =============================================================================
|
| 344 |
-
# ANALYTICS
|
| 345 |
-
# =============================================================================
|
| 346 |
-
|
| 347 |
-
def get_search_stats(self) -> Dict[str, Any]:
|
| 348 |
-
"""Get search index statistics"""
|
| 349 |
-
if not self.enabled or self.index is None:
|
| 350 |
-
return {
|
| 351 |
-
'enabled': False,
|
| 352 |
-
'total_articles_indexed': 0,
|
| 353 |
-
'embedding_dimension': 0
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
return {
|
| 357 |
-
'enabled': True,
|
| 358 |
-
'total_articles_indexed': len(self.article_ids),
|
| 359 |
-
'embedding_dimension': self.embedding_dim,
|
| 360 |
-
'index_size_mb': (self.index_path / 'faiss.index').stat().st_size / (1024 * 1024) if (self.index_path / 'faiss.index').exists() else 0
|
| 361 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,445 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Knowledge Base UI - Gradio interface for KB management
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import gradio as gr
|
| 6 |
-
from typing import List, Tuple
|
| 7 |
-
import pandas as pd
|
| 8 |
-
|
| 9 |
-
from .manager import KnowledgeBaseManager
|
| 10 |
-
from .search import SemanticSearchEngine
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class KnowledgeBaseUI:
|
| 14 |
-
"""Gradio UI for knowledge base"""
|
| 15 |
-
|
| 16 |
-
def __init__(self):
|
| 17 |
-
self.manager = KnowledgeBaseManager()
|
| 18 |
-
self.search_engine = SemanticSearchEngine()
|
| 19 |
-
|
| 20 |
-
def render(self) -> gr.Blocks:
|
| 21 |
-
"""Render complete KB interface"""
|
| 22 |
-
|
| 23 |
-
with gr.Blocks() as kb_interface:
|
| 24 |
-
gr.Markdown("# 📚 Knowledge Base")
|
| 25 |
-
|
| 26 |
-
with gr.Tabs():
|
| 27 |
-
# TAB 1: Browse Articles
|
| 28 |
-
with gr.Tab("📖 Articles"):
|
| 29 |
-
with gr.Row():
|
| 30 |
-
with gr.Column(scale=1):
|
| 31 |
-
category_filter = gr.Dropdown(
|
| 32 |
-
choices=['All'],
|
| 33 |
-
value='All',
|
| 34 |
-
label="Filter by Category"
|
| 35 |
-
)
|
| 36 |
-
with gr.Column(scale=1):
|
| 37 |
-
status_filter = gr.Dropdown(
|
| 38 |
-
choices=['All', 'published', 'draft', 'archived'],
|
| 39 |
-
value='published',
|
| 40 |
-
label="Filter by Status"
|
| 41 |
-
)
|
| 42 |
-
with gr.Column(scale=1):
|
| 43 |
-
refresh_articles_btn = gr.Button("🔄 Refresh", variant="secondary")
|
| 44 |
-
|
| 45 |
-
articles_table = gr.Dataframe(
|
| 46 |
-
headers=["ID", "Title", "Category", "Status", "Views", "Helpful %", "Author", "Published"],
|
| 47 |
-
label="Knowledge Base Articles",
|
| 48 |
-
interactive=False
|
| 49 |
-
)
|
| 50 |
-
|
| 51 |
-
# Metrics
|
| 52 |
-
with gr.Row():
|
| 53 |
-
total_articles_metric = gr.Textbox(label="Total Articles", value="0")
|
| 54 |
-
published_metric = gr.Textbox(label="Published", value="0")
|
| 55 |
-
total_views_metric = gr.Textbox(label="Total Views", value="0")
|
| 56 |
-
|
| 57 |
-
# TAB 2: Search (Semantic)
|
| 58 |
-
with gr.Tab("🔍 Search"):
|
| 59 |
-
gr.Markdown("### Semantic Search (RAG-Powered)")
|
| 60 |
-
gr.Markdown("Search using AI-powered semantic similarity")
|
| 61 |
-
|
| 62 |
-
search_query = gr.Textbox(
|
| 63 |
-
label="Search Query",
|
| 64 |
-
placeholder="How do I reset my password?",
|
| 65 |
-
lines=2
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
with gr.Row():
|
| 69 |
-
search_type = gr.Radio(
|
| 70 |
-
choices=['Semantic', 'Keyword', 'Hybrid'],
|
| 71 |
-
value='Semantic',
|
| 72 |
-
label="Search Type"
|
| 73 |
-
)
|
| 74 |
-
top_k = gr.Slider(
|
| 75 |
-
minimum=1,
|
| 76 |
-
maximum=10,
|
| 77 |
-
value=5,
|
| 78 |
-
step=1,
|
| 79 |
-
label="Number of Results"
|
| 80 |
-
)
|
| 81 |
-
|
| 82 |
-
search_btn = gr.Button("🔍 Search", variant="primary")
|
| 83 |
-
|
| 84 |
-
search_results = gr.Markdown("Enter a query and click Search")
|
| 85 |
-
|
| 86 |
-
# Search stats
|
| 87 |
-
with gr.Accordion("Search Engine Stats", open=False):
|
| 88 |
-
search_stats = gr.JSON(label="Index Statistics")
|
| 89 |
-
|
| 90 |
-
# TAB 3: Create Article
|
| 91 |
-
with gr.Tab("➕ Create Article"):
|
| 92 |
-
gr.Markdown("### Create New Knowledge Base Article")
|
| 93 |
-
|
| 94 |
-
with gr.Row():
|
| 95 |
-
with gr.Column():
|
| 96 |
-
article_title = gr.Textbox(
|
| 97 |
-
label="Title",
|
| 98 |
-
placeholder="How to..."
|
| 99 |
-
)
|
| 100 |
-
article_summary = gr.Textbox(
|
| 101 |
-
label="Summary",
|
| 102 |
-
placeholder="Brief description",
|
| 103 |
-
lines=2
|
| 104 |
-
)
|
| 105 |
-
article_content = gr.Textbox(
|
| 106 |
-
label="Content (Markdown)",
|
| 107 |
-
placeholder="# Getting Started\n\nDetailed instructions...",
|
| 108 |
-
lines=10
|
| 109 |
-
)
|
| 110 |
-
|
| 111 |
-
with gr.Column():
|
| 112 |
-
article_category = gr.Dropdown(
|
| 113 |
-
choices=['General', 'Technical', 'Billing', 'Account'],
|
| 114 |
-
value='General',
|
| 115 |
-
label="Category"
|
| 116 |
-
)
|
| 117 |
-
article_status = gr.Dropdown(
|
| 118 |
-
choices=['draft', 'published'],
|
| 119 |
-
value='draft',
|
| 120 |
-
label="Status"
|
| 121 |
-
)
|
| 122 |
-
article_visibility = gr.Dropdown(
|
| 123 |
-
choices=['public', 'internal', 'private'],
|
| 124 |
-
value='public',
|
| 125 |
-
label="Visibility"
|
| 126 |
-
)
|
| 127 |
-
article_tags = gr.Textbox(
|
| 128 |
-
label="Tags (comma-separated)",
|
| 129 |
-
placeholder="password, login, security"
|
| 130 |
-
)
|
| 131 |
-
article_author = gr.Textbox(
|
| 132 |
-
label="Author",
|
| 133 |
-
value="Support Team"
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
create_article_btn = gr.Button("Create Article", variant="primary")
|
| 137 |
-
create_article_output = gr.Textbox(label="Result", lines=2)
|
| 138 |
-
|
| 139 |
-
# TAB 4: View/Edit Article
|
| 140 |
-
with gr.Tab("✏️ Edit Article"):
|
| 141 |
-
with gr.Row():
|
| 142 |
-
edit_article_id = gr.Number(label="Article ID", precision=0)
|
| 143 |
-
load_article_btn = gr.Button("Load Article", variant="secondary")
|
| 144 |
-
|
| 145 |
-
with gr.Row():
|
| 146 |
-
with gr.Column():
|
| 147 |
-
edit_title = gr.Textbox(label="Title")
|
| 148 |
-
edit_summary = gr.Textbox(label="Summary", lines=2)
|
| 149 |
-
edit_content = gr.Textbox(label="Content", lines=10)
|
| 150 |
-
|
| 151 |
-
with gr.Column():
|
| 152 |
-
edit_status = gr.Dropdown(
|
| 153 |
-
choices=['draft', 'published', 'archived'],
|
| 154 |
-
label="Status"
|
| 155 |
-
)
|
| 156 |
-
edit_visibility = gr.Dropdown(
|
| 157 |
-
choices=['public', 'internal', 'private'],
|
| 158 |
-
label="Visibility"
|
| 159 |
-
)
|
| 160 |
-
changed_by = gr.Textbox(label="Changed By", value="Editor")
|
| 161 |
-
change_note = gr.Textbox(label="Change Note", placeholder="What did you change?")
|
| 162 |
-
|
| 163 |
-
update_article_btn = gr.Button("Update Article", variant="primary")
|
| 164 |
-
|
| 165 |
-
edit_output = gr.Textbox(label="Result", lines=2)
|
| 166 |
-
|
| 167 |
-
# Article versions
|
| 168 |
-
gr.Markdown("### 📜 Version History")
|
| 169 |
-
versions_table = gr.Dataframe(
|
| 170 |
-
headers=["Version", "Changed By", "Date", "Note"],
|
| 171 |
-
label="Versions",
|
| 172 |
-
interactive=False
|
| 173 |
-
)
|
| 174 |
-
|
| 175 |
-
# TAB 5: Index Management
|
| 176 |
-
with gr.Tab("⚙️ Index Management"):
|
| 177 |
-
gr.Markdown("### Semantic Search Index")
|
| 178 |
-
gr.Markdown("Build and manage the FAISS vector index for semantic search")
|
| 179 |
-
|
| 180 |
-
index_stats_display = gr.JSON(label="Index Statistics")
|
| 181 |
-
|
| 182 |
-
with gr.Row():
|
| 183 |
-
build_index_btn = gr.Button("🔨 Build Index", variant="primary")
|
| 184 |
-
rebuild_index_btn = gr.Button("🔄 Rebuild Index (Force)", variant="secondary")
|
| 185 |
-
|
| 186 |
-
index_output = gr.Textbox(label="Build Output", lines=5)
|
| 187 |
-
|
| 188 |
-
# =============================================================================
|
| 189 |
-
# EVENT HANDLERS
|
| 190 |
-
# =============================================================================
|
| 191 |
-
|
| 192 |
-
def load_articles(category, status):
|
| 193 |
-
"""Load articles with filters"""
|
| 194 |
-
try:
|
| 195 |
-
status_filter = None if status == 'All' else status
|
| 196 |
-
|
| 197 |
-
articles, total = self.manager.get_all_articles(
|
| 198 |
-
status=status_filter,
|
| 199 |
-
limit=100
|
| 200 |
-
)
|
| 201 |
-
|
| 202 |
-
rows = []
|
| 203 |
-
for article in articles:
|
| 204 |
-
# Calculate helpfulness percentage
|
| 205 |
-
total_votes = article.helpful_count + article.not_helpful_count
|
| 206 |
-
helpful_pct = f"{int(article.helpfulness_score() * 100)}%" if total_votes > 0 else "N/A"
|
| 207 |
-
|
| 208 |
-
rows.append([
|
| 209 |
-
article.id,
|
| 210 |
-
article.title[:60],
|
| 211 |
-
article.category.name if article.category else "Uncategorized",
|
| 212 |
-
article.status,
|
| 213 |
-
article.view_count,
|
| 214 |
-
helpful_pct,
|
| 215 |
-
article.author or "Unknown",
|
| 216 |
-
article.published_at.strftime("%Y-%m-%d") if article.published_at else "Not published"
|
| 217 |
-
])
|
| 218 |
-
|
| 219 |
-
# Get stats
|
| 220 |
-
stats = self.manager.get_kb_stats()
|
| 221 |
-
|
| 222 |
-
return (
|
| 223 |
-
rows,
|
| 224 |
-
str(stats['total_articles']),
|
| 225 |
-
str(stats['published_articles']),
|
| 226 |
-
str(stats['total_views'])
|
| 227 |
-
)
|
| 228 |
-
|
| 229 |
-
except Exception as e:
|
| 230 |
-
return [], f"Error: {e}", "0", "0"
|
| 231 |
-
|
| 232 |
-
def perform_search(query, search_type, top_k_val):
|
| 233 |
-
"""Perform semantic or keyword search"""
|
| 234 |
-
if not query:
|
| 235 |
-
return "Please enter a search query"
|
| 236 |
-
|
| 237 |
-
try:
|
| 238 |
-
if search_type == 'Semantic':
|
| 239 |
-
results = self.search_engine.search(query, top_k=int(top_k_val))
|
| 240 |
-
elif search_type == 'Keyword':
|
| 241 |
-
articles = self.manager.search_articles(query, limit=int(top_k_val))
|
| 242 |
-
results = [(article, 0.5) for article in articles]
|
| 243 |
-
else: # Hybrid
|
| 244 |
-
results = self.search_engine.hybrid_search(query, top_k=int(top_k_val))
|
| 245 |
-
|
| 246 |
-
if not results:
|
| 247 |
-
return "No results found"
|
| 248 |
-
|
| 249 |
-
# Format results
|
| 250 |
-
output = f"## Search Results for: '{query}'\n\n"
|
| 251 |
-
output += f"Found {len(results)} relevant articles:\n\n"
|
| 252 |
-
|
| 253 |
-
for i, (article, score) in enumerate(results, 1):
|
| 254 |
-
# Calculate helpfulness
|
| 255 |
-
total_votes = article.helpful_count + article.not_helpful_count
|
| 256 |
-
helpful_pct = int(article.helpfulness_score() * 100) if total_votes > 0 else 0
|
| 257 |
-
|
| 258 |
-
output += f"### {i}. {article.title}\n"
|
| 259 |
-
output += f"**Relevance Score:** {score:.2f} | "
|
| 260 |
-
output += f"**Views:** {article.view_count} | "
|
| 261 |
-
output += f"**Helpfulness:** {helpful_pct}%\n\n"
|
| 262 |
-
output += f"{article.summary or article.content[:200]}...\n\n"
|
| 263 |
-
output += f"[View Article #{article.id}]\n\n"
|
| 264 |
-
output += "---\n\n"
|
| 265 |
-
|
| 266 |
-
return output
|
| 267 |
-
|
| 268 |
-
except Exception as e:
|
| 269 |
-
return f"❌ Search error: {str(e)}"
|
| 270 |
-
|
| 271 |
-
def get_search_engine_stats():
|
| 272 |
-
"""Get search engine statistics"""
|
| 273 |
-
stats = self.search_engine.get_search_stats()
|
| 274 |
-
# Return same stats for both outputs
|
| 275 |
-
return stats, stats
|
| 276 |
-
|
| 277 |
-
def create_new_article(title, summary, content, category, status, visibility, tags, author):
|
| 278 |
-
"""Create new article"""
|
| 279 |
-
if not title or not content:
|
| 280 |
-
return "❌ Title and content are required"
|
| 281 |
-
|
| 282 |
-
try:
|
| 283 |
-
# Parse tags
|
| 284 |
-
tag_list = [t.strip() for t in tags.split(',')] if tags else None
|
| 285 |
-
|
| 286 |
-
article = self.manager.create_article(
|
| 287 |
-
title=title,
|
| 288 |
-
summary=summary,
|
| 289 |
-
content=content,
|
| 290 |
-
author=author,
|
| 291 |
-
tags=tag_list,
|
| 292 |
-
status=status,
|
| 293 |
-
visibility=visibility
|
| 294 |
-
)
|
| 295 |
-
|
| 296 |
-
# Update search index if published
|
| 297 |
-
if status == 'published':
|
| 298 |
-
self.search_engine.update_index_for_article(article.id)
|
| 299 |
-
|
| 300 |
-
return f"✅ Article #{article.id} created successfully! Slug: {article.slug}"
|
| 301 |
-
|
| 302 |
-
except Exception as e:
|
| 303 |
-
return f"❌ Error creating article: {str(e)}"
|
| 304 |
-
|
| 305 |
-
def load_article_for_edit(article_id):
|
| 306 |
-
"""Load article into edit form"""
|
| 307 |
-
if not article_id:
|
| 308 |
-
return "", "", "", "", "", []
|
| 309 |
-
|
| 310 |
-
try:
|
| 311 |
-
article = self.manager.get_article(int(article_id))
|
| 312 |
-
if not article:
|
| 313 |
-
return "Article not found", "", "", "", "", []
|
| 314 |
-
|
| 315 |
-
# Load versions
|
| 316 |
-
versions = self.manager.get_article_versions(int(article_id))
|
| 317 |
-
version_rows = []
|
| 318 |
-
for v in versions:
|
| 319 |
-
version_rows.append([
|
| 320 |
-
v.version,
|
| 321 |
-
v.changed_by or "Unknown",
|
| 322 |
-
v.created_at.strftime("%Y-%m-%d %H:%M") if v.created_at else "",
|
| 323 |
-
v.change_note or ""
|
| 324 |
-
])
|
| 325 |
-
|
| 326 |
-
return (
|
| 327 |
-
article.title,
|
| 328 |
-
article.summary or "",
|
| 329 |
-
article.content,
|
| 330 |
-
article.status,
|
| 331 |
-
article.visibility,
|
| 332 |
-
version_rows
|
| 333 |
-
)
|
| 334 |
-
|
| 335 |
-
except Exception as e:
|
| 336 |
-
return f"Error: {e}", "", "", "", "", []
|
| 337 |
-
|
| 338 |
-
def update_article_content(article_id, title, summary, content, status, visibility, changed_by_val, change_note_val):
|
| 339 |
-
"""Update article"""
|
| 340 |
-
if not article_id:
|
| 341 |
-
return "Please enter article ID"
|
| 342 |
-
|
| 343 |
-
try:
|
| 344 |
-
article = self.manager.update_article(
|
| 345 |
-
article_id=int(article_id),
|
| 346 |
-
title=title,
|
| 347 |
-
summary=summary,
|
| 348 |
-
content=content,
|
| 349 |
-
status=status,
|
| 350 |
-
visibility=visibility,
|
| 351 |
-
changed_by=changed_by_val,
|
| 352 |
-
change_note=change_note_val
|
| 353 |
-
)
|
| 354 |
-
|
| 355 |
-
# Update search index if published
|
| 356 |
-
if status == 'published':
|
| 357 |
-
self.search_engine.update_index_for_article(article.id)
|
| 358 |
-
|
| 359 |
-
return f"✅ Article #{article.id} updated successfully (version {article.version})"
|
| 360 |
-
|
| 361 |
-
except Exception as e:
|
| 362 |
-
return f"❌ Error: {str(e)}"
|
| 363 |
-
|
| 364 |
-
def build_search_index(force=False):
|
| 365 |
-
"""Build semantic search index"""
|
| 366 |
-
try:
|
| 367 |
-
output = "Building semantic search index...\n"
|
| 368 |
-
self.search_engine.build_index(force_rebuild=force)
|
| 369 |
-
stats = self.search_engine.get_search_stats()
|
| 370 |
-
output += f"\n✅ Index built successfully!\n"
|
| 371 |
-
output += f"Articles indexed: {stats.get('total_articles_indexed', 0)}\n"
|
| 372 |
-
output += f"Embedding dimension: {stats.get('embedding_dimension', 0)}\n"
|
| 373 |
-
return output
|
| 374 |
-
except Exception as e:
|
| 375 |
-
return f"❌ Error building index: {str(e)}"
|
| 376 |
-
|
| 377 |
-
# Wire up events
|
| 378 |
-
refresh_articles_btn.click(
|
| 379 |
-
fn=load_articles,
|
| 380 |
-
inputs=[category_filter, status_filter],
|
| 381 |
-
outputs=[articles_table, total_articles_metric, published_metric, total_views_metric]
|
| 382 |
-
)
|
| 383 |
-
|
| 384 |
-
status_filter.change(
|
| 385 |
-
fn=load_articles,
|
| 386 |
-
inputs=[category_filter, status_filter],
|
| 387 |
-
outputs=[articles_table, total_articles_metric, published_metric, total_views_metric]
|
| 388 |
-
)
|
| 389 |
-
|
| 390 |
-
search_btn.click(
|
| 391 |
-
fn=perform_search,
|
| 392 |
-
inputs=[search_query, search_type, top_k],
|
| 393 |
-
outputs=[search_results]
|
| 394 |
-
)
|
| 395 |
-
|
| 396 |
-
create_article_btn.click(
|
| 397 |
-
fn=create_new_article,
|
| 398 |
-
inputs=[article_title, article_summary, article_content, article_category,
|
| 399 |
-
article_status, article_visibility, article_tags, article_author],
|
| 400 |
-
outputs=[create_article_output]
|
| 401 |
-
)
|
| 402 |
-
|
| 403 |
-
load_article_btn.click(
|
| 404 |
-
fn=load_article_for_edit,
|
| 405 |
-
inputs=[edit_article_id],
|
| 406 |
-
outputs=[edit_title, edit_summary, edit_content, edit_status,
|
| 407 |
-
edit_visibility, versions_table]
|
| 408 |
-
)
|
| 409 |
-
|
| 410 |
-
update_article_btn.click(
|
| 411 |
-
fn=update_article_content,
|
| 412 |
-
inputs=[edit_article_id, edit_title, edit_summary, edit_content,
|
| 413 |
-
edit_status, edit_visibility, changed_by, change_note],
|
| 414 |
-
outputs=[edit_output]
|
| 415 |
-
)
|
| 416 |
-
|
| 417 |
-
build_index_btn.click(
|
| 418 |
-
fn=lambda: build_search_index(force=False),
|
| 419 |
-
outputs=[index_output]
|
| 420 |
-
)
|
| 421 |
-
|
| 422 |
-
rebuild_index_btn.click(
|
| 423 |
-
fn=lambda: build_search_index(force=True),
|
| 424 |
-
outputs=[index_output]
|
| 425 |
-
)
|
| 426 |
-
|
| 427 |
-
# Load initial data
|
| 428 |
-
kb_interface.load(
|
| 429 |
-
fn=load_articles,
|
| 430 |
-
inputs=[category_filter, status_filter],
|
| 431 |
-
outputs=[articles_table, total_articles_metric, published_metric, total_views_metric]
|
| 432 |
-
)
|
| 433 |
-
|
| 434 |
-
kb_interface.load(
|
| 435 |
-
fn=get_search_engine_stats,
|
| 436 |
-
outputs=[search_stats, index_stats_display]
|
| 437 |
-
)
|
| 438 |
-
|
| 439 |
-
return kb_interface
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
def render_kb_ui() -> gr.Blocks:
|
| 443 |
-
"""Convenience function to render KB UI"""
|
| 444 |
-
ui = KnowledgeBaseUI()
|
| 445 |
-
return ui.render()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Ticket Management Module
|
| 3 |
-
Handles customer support tickets, SLA tracking, and AI-powered routing
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from .manager import TicketManager
|
| 7 |
-
from .ui import render_ticket_ui
|
| 8 |
-
|
| 9 |
-
__all__ = ['TicketManager', 'render_ticket_ui']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,552 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Ticket Manager - Business logic for ticket operations
|
| 3 |
-
Handles CRUD, SLA tracking, routing, and AI categorization
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import Optional, List, Dict, Any, Tuple
|
| 7 |
-
from datetime import datetime, timedelta
|
| 8 |
-
from sqlalchemy.orm import Session
|
| 9 |
-
from sqlalchemy import and_, or_, desc, func
|
| 10 |
-
import json
|
| 11 |
-
|
| 12 |
-
from models.cx_models import CXTicket, CXTicketMessage, CXCustomer, CXInteraction
|
| 13 |
-
from database.manager import get_db_manager
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
class TicketManager:
|
| 17 |
-
"""Manages all ticket operations"""
|
| 18 |
-
|
| 19 |
-
def __init__(self):
|
| 20 |
-
self.db = get_db_manager()
|
| 21 |
-
|
| 22 |
-
# SLA configuration (in minutes)
|
| 23 |
-
self.sla_config = {
|
| 24 |
-
'urgent': {'first_response': 15, 'resolution': 120},
|
| 25 |
-
'high': {'first_response': 60, 'resolution': 480},
|
| 26 |
-
'medium': {'first_response': 240, 'resolution': 1440},
|
| 27 |
-
'low': {'first_response': 480, 'resolution': 2880}
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
# =============================================================================
|
| 31 |
-
# TICKET CRUD OPERATIONS
|
| 32 |
-
# =============================================================================
|
| 33 |
-
|
| 34 |
-
def create_ticket(
|
| 35 |
-
self,
|
| 36 |
-
customer_id: int,
|
| 37 |
-
subject: str,
|
| 38 |
-
description: str,
|
| 39 |
-
priority: str = 'medium',
|
| 40 |
-
category: Optional[str] = None,
|
| 41 |
-
source: str = 'manual',
|
| 42 |
-
tags: Optional[List[str]] = None
|
| 43 |
-
) -> CXTicket:
|
| 44 |
-
"""
|
| 45 |
-
Create a new support ticket
|
| 46 |
-
|
| 47 |
-
Args:
|
| 48 |
-
customer_id: ID of the customer creating the ticket
|
| 49 |
-
subject: Ticket subject line
|
| 50 |
-
description: Detailed description of the issue
|
| 51 |
-
priority: low, medium, high, urgent
|
| 52 |
-
category: Optional category (technical, billing, etc.)
|
| 53 |
-
source: manual, email, chat, api, web_form
|
| 54 |
-
tags: Optional list of tags
|
| 55 |
-
|
| 56 |
-
Returns:
|
| 57 |
-
Created ticket object
|
| 58 |
-
"""
|
| 59 |
-
with self.db.get_session() as session:
|
| 60 |
-
# Get customer
|
| 61 |
-
customer = session.query(CXCustomer).filter_by(id=customer_id).first()
|
| 62 |
-
if not customer:
|
| 63 |
-
raise ValueError(f"Customer {customer_id} not found")
|
| 64 |
-
|
| 65 |
-
# Calculate SLA due time
|
| 66 |
-
sla_due_at = self._calculate_sla_due(priority, 'resolution')
|
| 67 |
-
|
| 68 |
-
# Create ticket
|
| 69 |
-
ticket = CXTicket(
|
| 70 |
-
customer_id=customer_id,
|
| 71 |
-
subject=subject,
|
| 72 |
-
description=description,
|
| 73 |
-
priority=priority,
|
| 74 |
-
category=category,
|
| 75 |
-
source=source,
|
| 76 |
-
status='new',
|
| 77 |
-
sla_due_at=sla_due_at
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
if tags:
|
| 81 |
-
ticket.set_tags(tags)
|
| 82 |
-
|
| 83 |
-
session.add(ticket)
|
| 84 |
-
session.flush() # Get ticket ID
|
| 85 |
-
|
| 86 |
-
# Create initial system message
|
| 87 |
-
self.add_message(
|
| 88 |
-
session=session,
|
| 89 |
-
ticket_id=ticket.id,
|
| 90 |
-
sender_type='system',
|
| 91 |
-
sender_id='system',
|
| 92 |
-
sender_name='System',
|
| 93 |
-
message=f"Ticket created by {customer.full_name}",
|
| 94 |
-
is_internal=True
|
| 95 |
-
)
|
| 96 |
-
|
| 97 |
-
# Update customer stats
|
| 98 |
-
customer.total_tickets += 1
|
| 99 |
-
customer.last_interaction_at = datetime.utcnow()
|
| 100 |
-
|
| 101 |
-
# Create interaction record
|
| 102 |
-
interaction = CXInteraction(
|
| 103 |
-
customer_id=customer_id,
|
| 104 |
-
type='ticket',
|
| 105 |
-
channel='web',
|
| 106 |
-
summary=subject,
|
| 107 |
-
reference_type='ticket',
|
| 108 |
-
reference_id=ticket.id,
|
| 109 |
-
occurred_at=datetime.utcnow()
|
| 110 |
-
)
|
| 111 |
-
session.add(interaction)
|
| 112 |
-
|
| 113 |
-
session.commit()
|
| 114 |
-
session.refresh(ticket)
|
| 115 |
-
|
| 116 |
-
return ticket
|
| 117 |
-
|
| 118 |
-
def get_ticket(self, ticket_id: int) -> Optional[CXTicket]:
|
| 119 |
-
"""Get ticket by ID"""
|
| 120 |
-
with self.db.get_session() as session:
|
| 121 |
-
ticket = session.query(CXTicket).filter_by(id=ticket_id).first()
|
| 122 |
-
if ticket:
|
| 123 |
-
# Eagerly load relationships
|
| 124 |
-
_ = ticket.customer
|
| 125 |
-
_ = ticket.messages
|
| 126 |
-
return ticket
|
| 127 |
-
|
| 128 |
-
def get_all_tickets(
|
| 129 |
-
self,
|
| 130 |
-
status: Optional[str] = None,
|
| 131 |
-
priority: Optional[str] = None,
|
| 132 |
-
assigned_to: Optional[str] = None,
|
| 133 |
-
customer_id: Optional[int] = None,
|
| 134 |
-
limit: int = 100,
|
| 135 |
-
offset: int = 0
|
| 136 |
-
) -> Tuple[List[CXTicket], int]:
|
| 137 |
-
"""
|
| 138 |
-
Get tickets with optional filters
|
| 139 |
-
|
| 140 |
-
Returns:
|
| 141 |
-
Tuple of (tickets list, total count)
|
| 142 |
-
"""
|
| 143 |
-
with self.db.get_session() as session:
|
| 144 |
-
query = session.query(CXTicket)
|
| 145 |
-
|
| 146 |
-
# Apply filters
|
| 147 |
-
if status:
|
| 148 |
-
query = query.filter(CXTicket.status == status)
|
| 149 |
-
if priority:
|
| 150 |
-
query = query.filter(CXTicket.priority == priority)
|
| 151 |
-
if assigned_to:
|
| 152 |
-
query = query.filter(CXTicket.assigned_to == assigned_to)
|
| 153 |
-
if customer_id:
|
| 154 |
-
query = query.filter(CXTicket.customer_id == customer_id)
|
| 155 |
-
|
| 156 |
-
# Get total count
|
| 157 |
-
total = query.count()
|
| 158 |
-
|
| 159 |
-
# Apply pagination and ordering
|
| 160 |
-
tickets = query.order_by(desc(CXTicket.created_at)).limit(limit).offset(offset).all()
|
| 161 |
-
|
| 162 |
-
# Eagerly load customers
|
| 163 |
-
for ticket in tickets:
|
| 164 |
-
_ = ticket.customer
|
| 165 |
-
|
| 166 |
-
return tickets, total
|
| 167 |
-
|
| 168 |
-
def update_ticket(
|
| 169 |
-
self,
|
| 170 |
-
ticket_id: int,
|
| 171 |
-
**updates
|
| 172 |
-
) -> CXTicket:
|
| 173 |
-
"""
|
| 174 |
-
Update ticket fields
|
| 175 |
-
|
| 176 |
-
Args:
|
| 177 |
-
ticket_id: ID of ticket to update
|
| 178 |
-
**updates: Fields to update (status, priority, category, assigned_to, etc.)
|
| 179 |
-
"""
|
| 180 |
-
with self.db.get_session() as session:
|
| 181 |
-
ticket = session.query(CXTicket).filter_by(id=ticket_id).first()
|
| 182 |
-
if not ticket:
|
| 183 |
-
raise ValueError(f"Ticket {ticket_id} not found")
|
| 184 |
-
|
| 185 |
-
# Track status changes for metrics
|
| 186 |
-
old_status = ticket.status
|
| 187 |
-
|
| 188 |
-
# Update fields
|
| 189 |
-
for key, value in updates.items():
|
| 190 |
-
if hasattr(ticket, key):
|
| 191 |
-
setattr(ticket, key, value)
|
| 192 |
-
|
| 193 |
-
# Update timestamps based on status changes
|
| 194 |
-
if 'status' in updates:
|
| 195 |
-
new_status = updates['status']
|
| 196 |
-
|
| 197 |
-
if new_status == 'resolved' and old_status != 'resolved':
|
| 198 |
-
ticket.resolved_at = datetime.utcnow()
|
| 199 |
-
# Calculate resolution time
|
| 200 |
-
if ticket.created_at:
|
| 201 |
-
delta = datetime.utcnow() - ticket.created_at
|
| 202 |
-
ticket.resolution_time_minutes = int(delta.total_seconds() / 60)
|
| 203 |
-
|
| 204 |
-
elif new_status == 'closed' and old_status != 'closed':
|
| 205 |
-
ticket.closed_at = datetime.utcnow()
|
| 206 |
-
|
| 207 |
-
# Track reopened tickets
|
| 208 |
-
elif new_status == 'open' and old_status in ['resolved', 'closed']:
|
| 209 |
-
ticket.reopened_count += 1
|
| 210 |
-
|
| 211 |
-
session.commit()
|
| 212 |
-
session.refresh(ticket)
|
| 213 |
-
|
| 214 |
-
return ticket
|
| 215 |
-
|
| 216 |
-
def delete_ticket(self, ticket_id: int) -> bool:
|
| 217 |
-
"""Delete a ticket (soft delete - set status to 'deleted')"""
|
| 218 |
-
with self.db.get_session() as session:
|
| 219 |
-
ticket = session.query(CXTicket).filter_by(id=ticket_id).first()
|
| 220 |
-
if not ticket:
|
| 221 |
-
return False
|
| 222 |
-
|
| 223 |
-
ticket.status = 'deleted'
|
| 224 |
-
session.commit()
|
| 225 |
-
return True
|
| 226 |
-
|
| 227 |
-
# =============================================================================
|
| 228 |
-
# TICKET MESSAGES
|
| 229 |
-
# =============================================================================
|
| 230 |
-
|
| 231 |
-
def add_message(
|
| 232 |
-
self,
|
| 233 |
-
ticket_id: int = None,
|
| 234 |
-
session: Session = None,
|
| 235 |
-
sender_type: str = 'agent',
|
| 236 |
-
sender_id: Optional[str] = None,
|
| 237 |
-
sender_name: Optional[str] = None,
|
| 238 |
-
message: str = '',
|
| 239 |
-
is_internal: bool = False,
|
| 240 |
-
sentiment: Optional[str] = None,
|
| 241 |
-
intent: Optional[str] = None
|
| 242 |
-
) -> CXTicketMessage:
|
| 243 |
-
"""
|
| 244 |
-
Add a message to a ticket
|
| 245 |
-
|
| 246 |
-
Args:
|
| 247 |
-
ticket_id: ID of the ticket
|
| 248 |
-
session: Optional existing session (for transactions)
|
| 249 |
-
sender_type: customer, agent, system, ai_bot
|
| 250 |
-
sender_id: ID of the sender
|
| 251 |
-
sender_name: Display name of sender
|
| 252 |
-
message: Message content
|
| 253 |
-
is_internal: True for internal notes
|
| 254 |
-
sentiment: Optional sentiment (positive, neutral, negative)
|
| 255 |
-
intent: Optional intent (question, complaint, praise, feedback)
|
| 256 |
-
"""
|
| 257 |
-
def _add_msg(sess):
|
| 258 |
-
ticket = sess.query(CXTicket).filter_by(id=ticket_id).first()
|
| 259 |
-
if not ticket:
|
| 260 |
-
raise ValueError(f"Ticket {ticket_id} not found")
|
| 261 |
-
|
| 262 |
-
msg = CXTicketMessage(
|
| 263 |
-
ticket_id=ticket_id,
|
| 264 |
-
sender_type=sender_type,
|
| 265 |
-
sender_id=sender_id,
|
| 266 |
-
sender_name=sender_name,
|
| 267 |
-
message=message,
|
| 268 |
-
is_internal=is_internal,
|
| 269 |
-
sentiment=sentiment,
|
| 270 |
-
intent=intent
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
sess.add(msg)
|
| 274 |
-
|
| 275 |
-
# Update first response time if this is first agent response
|
| 276 |
-
if sender_type == 'agent' and not ticket.first_response_at:
|
| 277 |
-
ticket.first_response_at = datetime.utcnow()
|
| 278 |
-
if ticket.created_at:
|
| 279 |
-
delta = datetime.utcnow() - ticket.created_at
|
| 280 |
-
ticket.response_time_minutes = int(delta.total_seconds() / 60)
|
| 281 |
-
|
| 282 |
-
# Auto-open ticket if it's new
|
| 283 |
-
if ticket.status == 'new' and sender_type == 'agent':
|
| 284 |
-
ticket.status = 'open'
|
| 285 |
-
|
| 286 |
-
sess.flush()
|
| 287 |
-
return msg
|
| 288 |
-
|
| 289 |
-
if session:
|
| 290 |
-
return _add_msg(session)
|
| 291 |
-
else:
|
| 292 |
-
with self.db.get_session() as sess:
|
| 293 |
-
msg = _add_msg(sess)
|
| 294 |
-
sess.commit()
|
| 295 |
-
sess.refresh(msg)
|
| 296 |
-
return msg
|
| 297 |
-
|
| 298 |
-
def get_ticket_messages(
|
| 299 |
-
self,
|
| 300 |
-
ticket_id: int,
|
| 301 |
-
include_internal: bool = True
|
| 302 |
-
) -> List[CXTicketMessage]:
|
| 303 |
-
"""Get all messages for a ticket"""
|
| 304 |
-
with self.db.get_session() as session:
|
| 305 |
-
query = session.query(CXTicketMessage).filter(
|
| 306 |
-
CXTicketMessage.ticket_id == ticket_id
|
| 307 |
-
)
|
| 308 |
-
|
| 309 |
-
if not include_internal:
|
| 310 |
-
query = query.filter(CXTicketMessage.is_internal == False)
|
| 311 |
-
|
| 312 |
-
messages = query.order_by(CXTicketMessage.created_at).all()
|
| 313 |
-
return messages
|
| 314 |
-
|
| 315 |
-
# =============================================================================
|
| 316 |
-
# TICKET ASSIGNMENT & ROUTING
|
| 317 |
-
# =============================================================================
|
| 318 |
-
|
| 319 |
-
def assign_ticket(
|
| 320 |
-
self,
|
| 321 |
-
ticket_id: int,
|
| 322 |
-
agent_id: str,
|
| 323 |
-
agent_name: Optional[str] = None
|
| 324 |
-
) -> CXTicket:
|
| 325 |
-
"""Assign ticket to an agent"""
|
| 326 |
-
with self.db.get_session() as session:
|
| 327 |
-
ticket = session.query(CXTicket).filter_by(id=ticket_id).first()
|
| 328 |
-
if not ticket:
|
| 329 |
-
raise ValueError(f"Ticket {ticket_id} not found")
|
| 330 |
-
|
| 331 |
-
old_assignee = ticket.assigned_to
|
| 332 |
-
ticket.assigned_to = agent_id
|
| 333 |
-
|
| 334 |
-
# Add system message
|
| 335 |
-
if old_assignee:
|
| 336 |
-
msg = f"Ticket reassigned from {old_assignee} to {agent_name or agent_id}"
|
| 337 |
-
else:
|
| 338 |
-
msg = f"Ticket assigned to {agent_name or agent_id}"
|
| 339 |
-
|
| 340 |
-
self.add_message(
|
| 341 |
-
session=session,
|
| 342 |
-
ticket_id=ticket_id,
|
| 343 |
-
sender_type='system',
|
| 344 |
-
sender_id='system',
|
| 345 |
-
sender_name='System',
|
| 346 |
-
message=msg,
|
| 347 |
-
is_internal=True
|
| 348 |
-
)
|
| 349 |
-
|
| 350 |
-
# Update status if needed
|
| 351 |
-
if ticket.status == 'new':
|
| 352 |
-
ticket.status = 'open'
|
| 353 |
-
|
| 354 |
-
session.commit()
|
| 355 |
-
session.refresh(ticket)
|
| 356 |
-
|
| 357 |
-
return ticket
|
| 358 |
-
|
| 359 |
-
def auto_route_ticket(self, ticket_id: int) -> str:
|
| 360 |
-
"""
|
| 361 |
-
Automatically route ticket to best agent/team
|
| 362 |
-
Uses AI categorization and workload balancing
|
| 363 |
-
|
| 364 |
-
Returns:
|
| 365 |
-
agent_id that ticket was assigned to
|
| 366 |
-
"""
|
| 367 |
-
# TODO: Implement smart routing based on:
|
| 368 |
-
# 1. Ticket category
|
| 369 |
-
# 2. Agent skills/expertise
|
| 370 |
-
# 3. Current workload
|
| 371 |
-
# 4. SLA priority
|
| 372 |
-
|
| 373 |
-
# For now, simple round-robin
|
| 374 |
-
agent_id = "agent_1" # Placeholder
|
| 375 |
-
self.assign_ticket(ticket_id, agent_id, "Auto-Assigned Agent")
|
| 376 |
-
return agent_id
|
| 377 |
-
|
| 378 |
-
# =============================================================================
|
| 379 |
-
# SLA MANAGEMENT
|
| 380 |
-
# =============================================================================
|
| 381 |
-
|
| 382 |
-
def _calculate_sla_due(self, priority: str, sla_type: str) -> datetime:
|
| 383 |
-
"""Calculate SLA due time based on priority"""
|
| 384 |
-
minutes = self.sla_config.get(priority, {}).get(sla_type, 1440)
|
| 385 |
-
return datetime.utcnow() + timedelta(minutes=minutes)
|
| 386 |
-
|
| 387 |
-
def get_overdue_tickets(self) -> List[CXTicket]:
|
| 388 |
-
"""Get all tickets that are past their SLA"""
|
| 389 |
-
with self.db.get_session() as session:
|
| 390 |
-
now = datetime.utcnow()
|
| 391 |
-
tickets = session.query(CXTicket).filter(
|
| 392 |
-
and_(
|
| 393 |
-
CXTicket.sla_due_at < now,
|
| 394 |
-
CXTicket.status.in_(['new', 'open', 'pending'])
|
| 395 |
-
)
|
| 396 |
-
).order_by(CXTicket.sla_due_at).all()
|
| 397 |
-
|
| 398 |
-
return tickets
|
| 399 |
-
|
| 400 |
-
def get_tickets_by_sla_status(self) -> Dict[str, List[CXTicket]]:
|
| 401 |
-
"""
|
| 402 |
-
Categorize tickets by SLA status
|
| 403 |
-
|
| 404 |
-
Returns:
|
| 405 |
-
Dict with keys: 'breached', 'at_risk', 'on_track'
|
| 406 |
-
"""
|
| 407 |
-
with self.db.get_session() as session:
|
| 408 |
-
now = datetime.utcnow()
|
| 409 |
-
at_risk_threshold = now + timedelta(hours=2)
|
| 410 |
-
|
| 411 |
-
all_open = session.query(CXTicket).filter(
|
| 412 |
-
CXTicket.status.in_(['new', 'open', 'pending'])
|
| 413 |
-
).all()
|
| 414 |
-
|
| 415 |
-
result = {
|
| 416 |
-
'breached': [],
|
| 417 |
-
'at_risk': [],
|
| 418 |
-
'on_track': []
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
for ticket in all_open:
|
| 422 |
-
if not ticket.sla_due_at:
|
| 423 |
-
result['on_track'].append(ticket)
|
| 424 |
-
elif ticket.sla_due_at < now:
|
| 425 |
-
result['breached'].append(ticket)
|
| 426 |
-
elif ticket.sla_due_at < at_risk_threshold:
|
| 427 |
-
result['at_risk'].append(ticket)
|
| 428 |
-
else:
|
| 429 |
-
result['on_track'].append(ticket)
|
| 430 |
-
|
| 431 |
-
return result
|
| 432 |
-
|
| 433 |
-
# =============================================================================
|
| 434 |
-
# ANALYTICS & REPORTING
|
| 435 |
-
# =============================================================================
|
| 436 |
-
|
| 437 |
-
def get_ticket_stats(
|
| 438 |
-
self,
|
| 439 |
-
start_date: Optional[datetime] = None,
|
| 440 |
-
end_date: Optional[datetime] = None
|
| 441 |
-
) -> Dict[str, Any]:
|
| 442 |
-
"""Get ticket statistics for a date range"""
|
| 443 |
-
with self.db.get_session() as session:
|
| 444 |
-
query = session.query(CXTicket)
|
| 445 |
-
|
| 446 |
-
if start_date:
|
| 447 |
-
query = query.filter(CXTicket.created_at >= start_date)
|
| 448 |
-
if end_date:
|
| 449 |
-
query = query.filter(CXTicket.created_at <= end_date)
|
| 450 |
-
|
| 451 |
-
tickets = query.all()
|
| 452 |
-
|
| 453 |
-
# Calculate stats
|
| 454 |
-
total = len(tickets)
|
| 455 |
-
by_status = {}
|
| 456 |
-
by_priority = {}
|
| 457 |
-
resolved_times = []
|
| 458 |
-
response_times = []
|
| 459 |
-
|
| 460 |
-
for ticket in tickets:
|
| 461 |
-
# Count by status
|
| 462 |
-
by_status[ticket.status] = by_status.get(ticket.status, 0) + 1
|
| 463 |
-
|
| 464 |
-
# Count by priority
|
| 465 |
-
by_priority[ticket.priority] = by_priority.get(ticket.priority, 0) + 1
|
| 466 |
-
|
| 467 |
-
# Collect resolution times
|
| 468 |
-
if ticket.resolution_time_minutes:
|
| 469 |
-
resolved_times.append(ticket.resolution_time_minutes)
|
| 470 |
-
|
| 471 |
-
# Collect response times
|
| 472 |
-
if ticket.response_time_minutes:
|
| 473 |
-
response_times.append(ticket.response_time_minutes)
|
| 474 |
-
|
| 475 |
-
return {
|
| 476 |
-
'total_tickets': total,
|
| 477 |
-
'by_status': by_status,
|
| 478 |
-
'by_priority': by_priority,
|
| 479 |
-
'avg_resolution_time_minutes': sum(resolved_times) / len(resolved_times) if resolved_times else 0,
|
| 480 |
-
'avg_response_time_minutes': sum(response_times) / len(response_times) if response_times else 0,
|
| 481 |
-
'resolution_rate': by_status.get('resolved', 0) / total if total > 0 else 0
|
| 482 |
-
}
|
| 483 |
-
|
| 484 |
-
# =============================================================================
|
| 485 |
-
# AI FEATURES
|
| 486 |
-
# =============================================================================
|
| 487 |
-
|
| 488 |
-
def analyze_sentiment(self, text: str) -> str:
|
| 489 |
-
"""
|
| 490 |
-
Analyze sentiment of ticket description or message
|
| 491 |
-
|
| 492 |
-
Returns:
|
| 493 |
-
'positive', 'neutral', or 'negative'
|
| 494 |
-
"""
|
| 495 |
-
# TODO: Implement with AI agent
|
| 496 |
-
# For now, simple keyword-based
|
| 497 |
-
|
| 498 |
-
negative_keywords = ['angry', 'frustrated', 'terrible', 'worst', 'hate', 'disappointed']
|
| 499 |
-
positive_keywords = ['thanks', 'great', 'excellent', 'love', 'appreciate', 'wonderful']
|
| 500 |
-
|
| 501 |
-
text_lower = text.lower()
|
| 502 |
-
|
| 503 |
-
neg_count = sum(1 for word in negative_keywords if word in text_lower)
|
| 504 |
-
pos_count = sum(1 for word in positive_keywords if word in text_lower)
|
| 505 |
-
|
| 506 |
-
if neg_count > pos_count:
|
| 507 |
-
return 'negative'
|
| 508 |
-
elif pos_count > neg_count:
|
| 509 |
-
return 'positive'
|
| 510 |
-
else:
|
| 511 |
-
return 'neutral'
|
| 512 |
-
|
| 513 |
-
def suggest_category(self, subject: str, description: str) -> Tuple[str, float]:
|
| 514 |
-
"""
|
| 515 |
-
Suggest ticket category using AI
|
| 516 |
-
|
| 517 |
-
Returns:
|
| 518 |
-
Tuple of (suggested_category, confidence)
|
| 519 |
-
"""
|
| 520 |
-
# TODO: Implement with AI agent
|
| 521 |
-
# For now, simple keyword matching
|
| 522 |
-
|
| 523 |
-
categories = {
|
| 524 |
-
'technical': ['error', 'bug', 'crash', 'not working', 'broken', 'issue'],
|
| 525 |
-
'billing': ['invoice', 'payment', 'charge', 'refund', 'subscription', 'bill'],
|
| 526 |
-
'feature_request': ['feature', 'add', 'improve', 'enhancement', 'suggest'],
|
| 527 |
-
'account': ['login', 'password', 'access', 'account', 'profile']
|
| 528 |
-
}
|
| 529 |
-
|
| 530 |
-
text = f"{subject} {description}".lower()
|
| 531 |
-
|
| 532 |
-
scores = {}
|
| 533 |
-
for category, keywords in categories.items():
|
| 534 |
-
score = sum(1 for keyword in keywords if keyword in text)
|
| 535 |
-
scores[category] = score
|
| 536 |
-
|
| 537 |
-
if max(scores.values()) > 0:
|
| 538 |
-
best_category = max(scores, key=scores.get)
|
| 539 |
-
confidence = scores[best_category] / sum(scores.values())
|
| 540 |
-
return best_category, confidence
|
| 541 |
-
else:
|
| 542 |
-
return 'general', 0.5
|
| 543 |
-
|
| 544 |
-
def suggest_response(self, ticket_id: int) -> Optional[str]:
|
| 545 |
-
"""
|
| 546 |
-
Generate AI-powered response suggestion for ticket
|
| 547 |
-
|
| 548 |
-
Returns:
|
| 549 |
-
Suggested response text or None
|
| 550 |
-
"""
|
| 551 |
-
# TODO: Implement with AI agent using KB articles and canned responses
|
| 552 |
-
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,506 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Ticket UI - Gradio interface for ticket management
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
import gradio as gr
|
| 6 |
-
from typing import Optional, List, Tuple
|
| 7 |
-
from datetime import datetime, timedelta
|
| 8 |
-
import pandas as pd
|
| 9 |
-
|
| 10 |
-
from .manager import TicketManager
|
| 11 |
-
from models.cx_models import CXTicket, CXCustomer
|
| 12 |
-
from database.manager import get_db_manager
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class TicketUI:
|
| 16 |
-
"""Gradio UI components for ticket management"""
|
| 17 |
-
|
| 18 |
-
def __init__(self):
|
| 19 |
-
self.manager = TicketManager()
|
| 20 |
-
self.db = get_db_manager()
|
| 21 |
-
|
| 22 |
-
def render(self) -> gr.Blocks:
|
| 23 |
-
"""Render the complete ticket management UI"""
|
| 24 |
-
|
| 25 |
-
with gr.Blocks() as ticket_interface:
|
| 26 |
-
gr.Markdown("# 🎫 Ticket Management")
|
| 27 |
-
|
| 28 |
-
with gr.Tabs():
|
| 29 |
-
# TAB 1: Ticket List
|
| 30 |
-
with gr.Tab("📋 All Tickets"):
|
| 31 |
-
with gr.Row():
|
| 32 |
-
with gr.Column(scale=1):
|
| 33 |
-
status_filter = gr.Dropdown(
|
| 34 |
-
choices=['All', 'new', 'open', 'pending', 'resolved', 'closed'],
|
| 35 |
-
value='All',
|
| 36 |
-
label="Filter by Status"
|
| 37 |
-
)
|
| 38 |
-
with gr.Column(scale=1):
|
| 39 |
-
priority_filter = gr.Dropdown(
|
| 40 |
-
choices=['All', 'urgent', 'high', 'medium', 'low'],
|
| 41 |
-
value='All',
|
| 42 |
-
label="Filter by Priority"
|
| 43 |
-
)
|
| 44 |
-
with gr.Column(scale=1):
|
| 45 |
-
refresh_btn = gr.Button("🔄 Refresh", variant="secondary")
|
| 46 |
-
|
| 47 |
-
ticket_table = gr.Dataframe(
|
| 48 |
-
headers=[
|
| 49 |
-
"ID", "Customer", "Subject", "Status", "Priority",
|
| 50 |
-
"Category", "Assigned To", "Created", "SLA Status"
|
| 51 |
-
],
|
| 52 |
-
datatype=["number", "str", "str", "str", "str", "str", "str", "str", "str"],
|
| 53 |
-
label="Support Tickets",
|
| 54 |
-
interactive=False,
|
| 55 |
-
wrap=True
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
# Metrics row
|
| 59 |
-
with gr.Row():
|
| 60 |
-
total_tickets_metric = gr.Textbox(label="Total Tickets", value="0")
|
| 61 |
-
open_tickets_metric = gr.Textbox(label="Open Tickets", value="0")
|
| 62 |
-
overdue_tickets_metric = gr.Textbox(label="Overdue", value="0")
|
| 63 |
-
avg_response_metric = gr.Textbox(label="Avg Response Time", value="0 min")
|
| 64 |
-
|
| 65 |
-
# TAB 2: Create New Ticket
|
| 66 |
-
with gr.Tab("➕ Create Ticket"):
|
| 67 |
-
gr.Markdown("### Create New Support Ticket")
|
| 68 |
-
|
| 69 |
-
with gr.Row():
|
| 70 |
-
with gr.Column():
|
| 71 |
-
customer_email = gr.Textbox(
|
| 72 |
-
label="Customer Email",
|
| 73 |
-
placeholder="[email protected]"
|
| 74 |
-
)
|
| 75 |
-
ticket_subject = gr.Textbox(
|
| 76 |
-
label="Subject",
|
| 77 |
-
placeholder="Brief description of the issue"
|
| 78 |
-
)
|
| 79 |
-
ticket_description = gr.Textbox(
|
| 80 |
-
label="Description",
|
| 81 |
-
placeholder="Detailed description of the issue",
|
| 82 |
-
lines=5
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
with gr.Column():
|
| 86 |
-
ticket_priority = gr.Dropdown(
|
| 87 |
-
choices=['low', 'medium', 'high', 'urgent'],
|
| 88 |
-
value='medium',
|
| 89 |
-
label="Priority"
|
| 90 |
-
)
|
| 91 |
-
ticket_category = gr.Dropdown(
|
| 92 |
-
choices=['technical', 'billing', 'feature_request', 'account', 'general'],
|
| 93 |
-
value='general',
|
| 94 |
-
label="Category"
|
| 95 |
-
)
|
| 96 |
-
ticket_source = gr.Dropdown(
|
| 97 |
-
choices=['manual', 'email', 'chat', 'web_form', 'api'],
|
| 98 |
-
value='manual',
|
| 99 |
-
label="Source"
|
| 100 |
-
)
|
| 101 |
-
|
| 102 |
-
create_ticket_btn = gr.Button("Create Ticket", variant="primary")
|
| 103 |
-
create_ticket_output = gr.Textbox(label="Result", lines=2)
|
| 104 |
-
|
| 105 |
-
# TAB 3: Ticket Details
|
| 106 |
-
with gr.Tab("🔍 Ticket Details"):
|
| 107 |
-
with gr.Row():
|
| 108 |
-
ticket_id_input = gr.Number(label="Ticket ID", precision=0)
|
| 109 |
-
load_ticket_btn = gr.Button("Load Ticket", variant="secondary")
|
| 110 |
-
|
| 111 |
-
with gr.Row():
|
| 112 |
-
with gr.Column(scale=2):
|
| 113 |
-
ticket_info = gr.Markdown("Select a ticket to view details")
|
| 114 |
-
|
| 115 |
-
gr.Markdown("### 💬 Conversation")
|
| 116 |
-
ticket_messages = gr.Dataframe(
|
| 117 |
-
headers=["Time", "Sender", "Message", "Type"],
|
| 118 |
-
label="Messages",
|
| 119 |
-
interactive=False
|
| 120 |
-
)
|
| 121 |
-
|
| 122 |
-
# Reply section
|
| 123 |
-
with gr.Row():
|
| 124 |
-
reply_message = gr.Textbox(
|
| 125 |
-
label="Add Reply",
|
| 126 |
-
placeholder="Type your response...",
|
| 127 |
-
lines=3
|
| 128 |
-
)
|
| 129 |
-
with gr.Row():
|
| 130 |
-
reply_as_agent_btn = gr.Button("Send as Agent", variant="primary")
|
| 131 |
-
add_internal_note_btn = gr.Button("Add Internal Note")
|
| 132 |
-
|
| 133 |
-
with gr.Column(scale=1):
|
| 134 |
-
gr.Markdown("### ⚙️ Actions")
|
| 135 |
-
|
| 136 |
-
update_status = gr.Dropdown(
|
| 137 |
-
choices=['new', 'open', 'pending', 'resolved', 'closed'],
|
| 138 |
-
label="Update Status"
|
| 139 |
-
)
|
| 140 |
-
update_status_btn = gr.Button("Update Status")
|
| 141 |
-
|
| 142 |
-
update_priority = gr.Dropdown(
|
| 143 |
-
choices=['low', 'medium', 'high', 'urgent'],
|
| 144 |
-
label="Update Priority"
|
| 145 |
-
)
|
| 146 |
-
update_priority_btn = gr.Button("Update Priority")
|
| 147 |
-
|
| 148 |
-
assign_to_input = gr.Textbox(label="Assign To (Agent ID)")
|
| 149 |
-
assign_btn = gr.Button("Assign Ticket")
|
| 150 |
-
|
| 151 |
-
action_output = gr.Textbox(label="Action Result", lines=2)
|
| 152 |
-
|
| 153 |
-
# TAB 4: SLA Dashboard
|
| 154 |
-
with gr.Tab("⏰ SLA Dashboard"):
|
| 155 |
-
gr.Markdown("## Service Level Agreement Monitoring")
|
| 156 |
-
|
| 157 |
-
refresh_sla_btn = gr.Button("🔄 Refresh SLA Status", variant="secondary")
|
| 158 |
-
|
| 159 |
-
with gr.Row():
|
| 160 |
-
breached_count = gr.Textbox(label="🔴 SLA Breached", value="0")
|
| 161 |
-
at_risk_count = gr.Textbox(label="🟡 At Risk", value="0")
|
| 162 |
-
on_track_count = gr.Textbox(label="🟢 On Track", value="0")
|
| 163 |
-
|
| 164 |
-
gr.Markdown("### 🔴 Breached SLA Tickets")
|
| 165 |
-
breached_table = gr.Dataframe(
|
| 166 |
-
headers=["ID", "Customer", "Subject", "Priority", "Due", "Overdue By"],
|
| 167 |
-
label="Tickets Past SLA",
|
| 168 |
-
interactive=False
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
gr.Markdown("### 🟡 At Risk Tickets (Due within 2 hours)")
|
| 172 |
-
at_risk_table = gr.Dataframe(
|
| 173 |
-
headers=["ID", "Customer", "Subject", "Priority", "Due In"],
|
| 174 |
-
label="Tickets At Risk",
|
| 175 |
-
interactive=False
|
| 176 |
-
)
|
| 177 |
-
|
| 178 |
-
# =============================================================================
|
| 179 |
-
# EVENT HANDLERS
|
| 180 |
-
# =============================================================================
|
| 181 |
-
|
| 182 |
-
def load_ticket_list(status_filter_val, priority_filter_val):
|
| 183 |
-
"""Load and display tickets based on filters"""
|
| 184 |
-
try:
|
| 185 |
-
status = None if status_filter_val == 'All' else status_filter_val
|
| 186 |
-
priority = None if priority_filter_val == 'All' else priority_filter_val
|
| 187 |
-
|
| 188 |
-
tickets, total = self.manager.get_all_tickets(
|
| 189 |
-
status=status,
|
| 190 |
-
priority=priority,
|
| 191 |
-
limit=100
|
| 192 |
-
)
|
| 193 |
-
except Exception as e:
|
| 194 |
-
error_msg = f"Database error: {str(e)}"
|
| 195 |
-
return [[error_msg, "", "", "", "", "", "", "", ""]], "Error", "0", "0", "0 min"
|
| 196 |
-
|
| 197 |
-
# Build table data
|
| 198 |
-
rows = []
|
| 199 |
-
open_count = 0
|
| 200 |
-
overdue_count = 0
|
| 201 |
-
|
| 202 |
-
for ticket in tickets:
|
| 203 |
-
# SLA status
|
| 204 |
-
if ticket.is_overdue():
|
| 205 |
-
sla_status = "🔴 OVERDUE"
|
| 206 |
-
overdue_count += 1
|
| 207 |
-
elif ticket.sla_due_at:
|
| 208 |
-
time_left = ticket.sla_due_at - datetime.utcnow()
|
| 209 |
-
if time_left.total_seconds() < 7200: # 2 hours
|
| 210 |
-
sla_status = f"🟡 {int(time_left.total_seconds() / 60)} min"
|
| 211 |
-
else:
|
| 212 |
-
sla_status = f"🟢 {int(time_left.total_seconds() / 3600)} hrs"
|
| 213 |
-
else:
|
| 214 |
-
sla_status = "N/A"
|
| 215 |
-
|
| 216 |
-
if ticket.status in ['new', 'open', 'pending']:
|
| 217 |
-
open_count += 1
|
| 218 |
-
|
| 219 |
-
rows.append([
|
| 220 |
-
ticket.id,
|
| 221 |
-
ticket.customer.full_name if ticket.customer else "Unknown",
|
| 222 |
-
ticket.subject[:50],
|
| 223 |
-
self._format_status_badge(ticket.status),
|
| 224 |
-
self._format_priority_badge(ticket.priority),
|
| 225 |
-
ticket.category or "N/A",
|
| 226 |
-
ticket.assigned_to or "Unassigned",
|
| 227 |
-
ticket.created_at.strftime("%Y-%m-%d %H:%M") if ticket.created_at else "",
|
| 228 |
-
sla_status
|
| 229 |
-
])
|
| 230 |
-
|
| 231 |
-
# Calculate avg response time
|
| 232 |
-
stats = self.manager.get_ticket_stats()
|
| 233 |
-
avg_response = int(stats.get('avg_response_time_minutes', 0))
|
| 234 |
-
|
| 235 |
-
return (
|
| 236 |
-
rows,
|
| 237 |
-
str(total),
|
| 238 |
-
str(open_count),
|
| 239 |
-
str(overdue_count),
|
| 240 |
-
f"{avg_response} min"
|
| 241 |
-
)
|
| 242 |
-
|
| 243 |
-
def create_new_ticket(email, subject, description, priority, category, source):
|
| 244 |
-
"""Create a new ticket"""
|
| 245 |
-
try:
|
| 246 |
-
# Find or create customer
|
| 247 |
-
with self.db.get_session() as session:
|
| 248 |
-
customer = session.query(CXCustomer).filter_by(email=email).first()
|
| 249 |
-
|
| 250 |
-
if not customer:
|
| 251 |
-
# Create new customer
|
| 252 |
-
customer = CXCustomer(
|
| 253 |
-
email=email,
|
| 254 |
-
first_interaction_at=datetime.utcnow()
|
| 255 |
-
)
|
| 256 |
-
session.add(customer)
|
| 257 |
-
session.commit()
|
| 258 |
-
session.refresh(customer)
|
| 259 |
-
|
| 260 |
-
customer_id = customer.id
|
| 261 |
-
|
| 262 |
-
# Create ticket
|
| 263 |
-
ticket = self.manager.create_ticket(
|
| 264 |
-
customer_id=customer_id,
|
| 265 |
-
subject=subject,
|
| 266 |
-
description=description,
|
| 267 |
-
priority=priority,
|
| 268 |
-
category=category,
|
| 269 |
-
source=source
|
| 270 |
-
)
|
| 271 |
-
|
| 272 |
-
return f"✅ Ticket #{ticket.id} created successfully for {email}"
|
| 273 |
-
|
| 274 |
-
except Exception as e:
|
| 275 |
-
return f"❌ Error creating ticket: {str(e)}"
|
| 276 |
-
|
| 277 |
-
def load_ticket_details(ticket_id):
|
| 278 |
-
"""Load detailed ticket information"""
|
| 279 |
-
if not ticket_id:
|
| 280 |
-
return "Please enter a ticket ID", []
|
| 281 |
-
|
| 282 |
-
try:
|
| 283 |
-
ticket = self.manager.get_ticket(int(ticket_id))
|
| 284 |
-
if not ticket:
|
| 285 |
-
return "❌ Ticket not found", []
|
| 286 |
-
|
| 287 |
-
# Build ticket info markdown
|
| 288 |
-
info = f"""
|
| 289 |
-
### Ticket #{ticket.id}: {ticket.subject}
|
| 290 |
-
|
| 291 |
-
**Customer:** {ticket.customer.full_name if ticket.customer else 'Unknown'} ({ticket.customer.email if ticket.customer else 'N/A'})
|
| 292 |
-
**Status:** {self._format_status_badge(ticket.status)}
|
| 293 |
-
**Priority:** {self._format_priority_badge(ticket.priority)}
|
| 294 |
-
**Category:** {ticket.category or 'N/A'}
|
| 295 |
-
**Assigned To:** {ticket.assigned_to or 'Unassigned'}
|
| 296 |
-
**Created:** {ticket.created_at.strftime("%Y-%m-%d %H:%M") if ticket.created_at else 'N/A'}
|
| 297 |
-
**SLA Due:** {ticket.sla_due_at.strftime("%Y-%m-%d %H:%M") if ticket.sla_due_at else 'N/A'}
|
| 298 |
-
**Sentiment:** {ticket.sentiment or 'Unknown'}
|
| 299 |
-
|
| 300 |
-
**Description:**
|
| 301 |
-
{ticket.description}
|
| 302 |
-
"""
|
| 303 |
-
|
| 304 |
-
# Load messages
|
| 305 |
-
messages = self.manager.get_ticket_messages(int(ticket_id))
|
| 306 |
-
message_rows = []
|
| 307 |
-
|
| 308 |
-
for msg in messages:
|
| 309 |
-
msg_type = "🔒 Internal" if msg.is_internal else "💬 Public"
|
| 310 |
-
message_rows.append([
|
| 311 |
-
msg.created_at.strftime("%H:%M") if msg.created_at else "",
|
| 312 |
-
msg.sender_name or msg.sender_type,
|
| 313 |
-
msg.message[:100],
|
| 314 |
-
msg_type
|
| 315 |
-
])
|
| 316 |
-
|
| 317 |
-
return info, message_rows
|
| 318 |
-
|
| 319 |
-
except Exception as e:
|
| 320 |
-
return f"❌ Error loading ticket: {str(e)}", []
|
| 321 |
-
|
| 322 |
-
def add_reply(ticket_id, message, is_internal=False):
|
| 323 |
-
"""Add a reply to ticket"""
|
| 324 |
-
if not ticket_id or not message:
|
| 325 |
-
return "Please enter ticket ID and message"
|
| 326 |
-
|
| 327 |
-
try:
|
| 328 |
-
self.manager.add_message(
|
| 329 |
-
ticket_id=int(ticket_id),
|
| 330 |
-
sender_type='agent',
|
| 331 |
-
sender_id='agent_1',
|
| 332 |
-
sender_name='Support Agent',
|
| 333 |
-
message=message,
|
| 334 |
-
is_internal=is_internal
|
| 335 |
-
)
|
| 336 |
-
return f"✅ {'Internal note' if is_internal else 'Reply'} added successfully"
|
| 337 |
-
except Exception as e:
|
| 338 |
-
return f"❌ Error: {str(e)}"
|
| 339 |
-
|
| 340 |
-
def update_ticket_field(ticket_id, field, value):
|
| 341 |
-
"""Update a ticket field"""
|
| 342 |
-
if not ticket_id or not value:
|
| 343 |
-
return "Please provide ticket ID and value"
|
| 344 |
-
|
| 345 |
-
try:
|
| 346 |
-
self.manager.update_ticket(int(ticket_id), **{field: value})
|
| 347 |
-
return f"✅ Ticket {field} updated to: {value}"
|
| 348 |
-
except Exception as e:
|
| 349 |
-
return f"❌ Error: {str(e)}"
|
| 350 |
-
|
| 351 |
-
def load_sla_dashboard():
|
| 352 |
-
"""Load SLA dashboard data"""
|
| 353 |
-
try:
|
| 354 |
-
sla_tickets = self.manager.get_tickets_by_sla_status()
|
| 355 |
-
|
| 356 |
-
breached = sla_tickets['breached']
|
| 357 |
-
at_risk = sla_tickets['at_risk']
|
| 358 |
-
on_track = sla_tickets['on_track']
|
| 359 |
-
|
| 360 |
-
# Build breached table
|
| 361 |
-
breached_rows = []
|
| 362 |
-
for ticket in breached:
|
| 363 |
-
overdue = datetime.utcnow() - ticket.sla_due_at
|
| 364 |
-
overdue_str = f"{int(overdue.total_seconds() / 3600)}h {int((overdue.total_seconds() % 3600) / 60)}m"
|
| 365 |
-
|
| 366 |
-
breached_rows.append([
|
| 367 |
-
ticket.id,
|
| 368 |
-
ticket.customer.full_name if ticket.customer else "Unknown",
|
| 369 |
-
ticket.subject[:40],
|
| 370 |
-
ticket.priority,
|
| 371 |
-
ticket.sla_due_at.strftime("%Y-%m-%d %H:%M") if ticket.sla_due_at else "",
|
| 372 |
-
overdue_str
|
| 373 |
-
])
|
| 374 |
-
|
| 375 |
-
# Build at-risk table
|
| 376 |
-
at_risk_rows = []
|
| 377 |
-
for ticket in at_risk:
|
| 378 |
-
time_left = ticket.sla_due_at - datetime.utcnow()
|
| 379 |
-
due_in = f"{int(time_left.total_seconds() / 60)} min"
|
| 380 |
-
|
| 381 |
-
at_risk_rows.append([
|
| 382 |
-
ticket.id,
|
| 383 |
-
ticket.customer.full_name if ticket.customer else "Unknown",
|
| 384 |
-
ticket.subject[:40],
|
| 385 |
-
ticket.priority,
|
| 386 |
-
due_in
|
| 387 |
-
])
|
| 388 |
-
|
| 389 |
-
return (
|
| 390 |
-
str(len(breached)),
|
| 391 |
-
str(len(at_risk)),
|
| 392 |
-
str(len(on_track)),
|
| 393 |
-
breached_rows,
|
| 394 |
-
at_risk_rows
|
| 395 |
-
)
|
| 396 |
-
|
| 397 |
-
except Exception as e:
|
| 398 |
-
return "Error", "Error", "Error", [], []
|
| 399 |
-
|
| 400 |
-
# Wire up events
|
| 401 |
-
refresh_btn.click(
|
| 402 |
-
fn=load_ticket_list,
|
| 403 |
-
inputs=[status_filter, priority_filter],
|
| 404 |
-
outputs=[ticket_table, total_tickets_metric, open_tickets_metric,
|
| 405 |
-
overdue_tickets_metric, avg_response_metric]
|
| 406 |
-
)
|
| 407 |
-
|
| 408 |
-
status_filter.change(
|
| 409 |
-
fn=load_ticket_list,
|
| 410 |
-
inputs=[status_filter, priority_filter],
|
| 411 |
-
outputs=[ticket_table, total_tickets_metric, open_tickets_metric,
|
| 412 |
-
overdue_tickets_metric, avg_response_metric]
|
| 413 |
-
)
|
| 414 |
-
|
| 415 |
-
priority_filter.change(
|
| 416 |
-
fn=load_ticket_list,
|
| 417 |
-
inputs=[status_filter, priority_filter],
|
| 418 |
-
outputs=[ticket_table, total_tickets_metric, open_tickets_metric,
|
| 419 |
-
overdue_tickets_metric, avg_response_metric]
|
| 420 |
-
)
|
| 421 |
-
|
| 422 |
-
create_ticket_btn.click(
|
| 423 |
-
fn=create_new_ticket,
|
| 424 |
-
inputs=[customer_email, ticket_subject, ticket_description,
|
| 425 |
-
ticket_priority, ticket_category, ticket_source],
|
| 426 |
-
outputs=[create_ticket_output]
|
| 427 |
-
)
|
| 428 |
-
|
| 429 |
-
load_ticket_btn.click(
|
| 430 |
-
fn=load_ticket_details,
|
| 431 |
-
inputs=[ticket_id_input],
|
| 432 |
-
outputs=[ticket_info, ticket_messages]
|
| 433 |
-
)
|
| 434 |
-
|
| 435 |
-
reply_as_agent_btn.click(
|
| 436 |
-
fn=lambda tid, msg: add_reply(tid, msg, False),
|
| 437 |
-
inputs=[ticket_id_input, reply_message],
|
| 438 |
-
outputs=[action_output]
|
| 439 |
-
)
|
| 440 |
-
|
| 441 |
-
add_internal_note_btn.click(
|
| 442 |
-
fn=lambda tid, msg: add_reply(tid, msg, True),
|
| 443 |
-
inputs=[ticket_id_input, reply_message],
|
| 444 |
-
outputs=[action_output]
|
| 445 |
-
)
|
| 446 |
-
|
| 447 |
-
update_status_btn.click(
|
| 448 |
-
fn=lambda tid, val: update_ticket_field(tid, 'status', val),
|
| 449 |
-
inputs=[ticket_id_input, update_status],
|
| 450 |
-
outputs=[action_output]
|
| 451 |
-
)
|
| 452 |
-
|
| 453 |
-
update_priority_btn.click(
|
| 454 |
-
fn=lambda tid, val: update_ticket_field(tid, 'priority', val),
|
| 455 |
-
inputs=[ticket_id_input, update_priority],
|
| 456 |
-
outputs=[action_output]
|
| 457 |
-
)
|
| 458 |
-
|
| 459 |
-
assign_btn.click(
|
| 460 |
-
fn=lambda tid, agent: update_ticket_field(tid, 'assigned_to', agent),
|
| 461 |
-
inputs=[ticket_id_input, assign_to_input],
|
| 462 |
-
outputs=[action_output]
|
| 463 |
-
)
|
| 464 |
-
|
| 465 |
-
refresh_sla_btn.click(
|
| 466 |
-
fn=load_sla_dashboard,
|
| 467 |
-
outputs=[breached_count, at_risk_count, on_track_count,
|
| 468 |
-
breached_table, at_risk_table]
|
| 469 |
-
)
|
| 470 |
-
|
| 471 |
-
# Load initial data
|
| 472 |
-
ticket_interface.load(
|
| 473 |
-
fn=load_ticket_list,
|
| 474 |
-
inputs=[status_filter, priority_filter],
|
| 475 |
-
outputs=[ticket_table, total_tickets_metric, open_tickets_metric,
|
| 476 |
-
overdue_tickets_metric, avg_response_metric]
|
| 477 |
-
)
|
| 478 |
-
|
| 479 |
-
return ticket_interface
|
| 480 |
-
|
| 481 |
-
def _format_status_badge(self, status: str) -> str:
|
| 482 |
-
"""Format status with emoji"""
|
| 483 |
-
badges = {
|
| 484 |
-
'new': '🆕 New',
|
| 485 |
-
'open': '📂 Open',
|
| 486 |
-
'pending': '⏸️ Pending',
|
| 487 |
-
'resolved': '✅ Resolved',
|
| 488 |
-
'closed': '🔒 Closed'
|
| 489 |
-
}
|
| 490 |
-
return badges.get(status, status)
|
| 491 |
-
|
| 492 |
-
def _format_priority_badge(self, priority: str) -> str:
|
| 493 |
-
"""Format priority with emoji"""
|
| 494 |
-
badges = {
|
| 495 |
-
'urgent': '🔴 Urgent',
|
| 496 |
-
'high': '🟠 High',
|
| 497 |
-
'medium': '🟡 Medium',
|
| 498 |
-
'low': '🟢 Low'
|
| 499 |
-
}
|
| 500 |
-
return badges.get(priority, priority)
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
def render_ticket_ui() -> gr.Blocks:
|
| 504 |
-
"""Convenience function to render ticket UI"""
|
| 505 |
-
ui = TicketUI()
|
| 506 |
-
return ui.render()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|