muzakkirhussain011 Claude commited on
Commit
d8dc6d3
·
1 Parent(s): 98a1db6

Remove all non-MCP modules (CX platform components)

Browse files

ARCHITECTURAL 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 CHANGED
@@ -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": []
CX_PLATFORM_ARCHITECTURE.md DELETED
@@ -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?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CX_PLATFORM_SUMMARY.md DELETED
@@ -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!**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -61,23 +61,17 @@ else:
61
 
62
  print("="*80 + "\n")
63
 
64
- # Initialize CX database FIRST (before importing modules that use it)
65
  from database.manager import get_db_manager
66
 
67
  try:
68
  db_manager = get_db_manager()
69
- print("✅ CX Platform database initialized")
70
  except Exception as e:
71
- print(f"⚠️ CX Platform database initialization error: {e}")
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")
app_enterprise.py DELETED
@@ -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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/init_db.py DELETED
@@ -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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
database/manager.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Database Manager for Enterprise CX AI Agent
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):
models/cx_models.py DELETED
@@ -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())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/analytics/__init__.py DELETED
@@ -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']
 
 
 
 
 
 
 
 
 
 
modules/analytics/manager.py DELETED
@@ -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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/analytics/ui.py DELETED
@@ -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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/chat/__init__.py DELETED
@@ -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']
 
 
 
 
 
 
 
 
 
 
 
modules/chat/bot.py DELETED
@@ -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
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/chat/manager.py DELETED
@@ -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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/chat/ui.py DELETED
@@ -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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/knowledge/__init__.py DELETED
@@ -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']
 
 
 
 
 
 
 
 
 
 
 
modules/knowledge/manager.py DELETED
@@ -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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/knowledge/search.py DELETED
@@ -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
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/knowledge/ui.py DELETED
@@ -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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/tickets/__init__.py DELETED
@@ -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']
 
 
 
 
 
 
 
 
 
 
modules/tickets/manager.py DELETED
@@ -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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/tickets/ui.py DELETED
@@ -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()