mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-17 12:20:04 +00:00
Some checks failed
Publish AI SDK / publish (push) Has been cancelled
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1047 lines
38 KiB
Text
1047 lines
38 KiB
Text
---
|
|
title: "Customer Support Bot"
|
|
description: "Build an intelligent support system that remembers customer history and provides personalized help"
|
|
---
|
|
|
|
Create a customer support system that remembers every interaction, tracks issues across conversations, and provides personalized support based on customer history and preferences.
|
|
|
|
## What You'll Build
|
|
|
|
A customer support bot that:
|
|
- **Remembers customer history** across all conversations and channels
|
|
- **Tracks ongoing issues** and follows up automatically
|
|
- **Provides personalized responses** based on customer tier and preferences
|
|
- **Escalates complex issues** to human agents with full context
|
|
- **Learns from resolutions** to improve future responses
|
|
|
|
## Prerequisites
|
|
|
|
- Node.js 18+ or Python 3.8+
|
|
- Supermemory API key
|
|
- OpenAI API key
|
|
- Customer database or CRM integration
|
|
- Basic understanding of customer support workflows
|
|
|
|
## Implementation
|
|
|
|
### Step 1: Customer Context Management
|
|
|
|
<Tabs>
|
|
<Tab title="Next.js">
|
|
```typescript lib/customer-context.ts
|
|
import { Supermemory } from 'supermemory'
|
|
|
|
const client = new Supermemory({
|
|
apiKey: process.env.SUPERMEMORY_API_KEY!
|
|
})
|
|
|
|
interface Customer {
|
|
id: string
|
|
email: string
|
|
name: string
|
|
tier: 'free' | 'pro' | 'enterprise'
|
|
joinDate: string
|
|
preferences?: Record<string, any>
|
|
}
|
|
|
|
interface SupportTicket {
|
|
id: string
|
|
customerId: string
|
|
subject: string
|
|
status: 'open' | 'pending' | 'resolved' | 'closed'
|
|
priority: 'low' | 'medium' | 'high' | 'urgent'
|
|
category: string
|
|
createdAt: string
|
|
updatedAt: string
|
|
assignedAgent?: string
|
|
}
|
|
|
|
export class CustomerContextManager {
|
|
private getContainerTag(customerId: string): string {
|
|
return `customer_${customerId}`
|
|
}
|
|
|
|
async addInteraction(customerId: string, interaction: {
|
|
type: 'chat' | 'email' | 'phone' | 'ticket'
|
|
content: string
|
|
channel: string
|
|
outcome?: 'resolved' | 'escalated' | 'pending'
|
|
agentId?: string
|
|
metadata?: Record<string, any>
|
|
}) {
|
|
try {
|
|
const result = await client.add({
|
|
content: `${interaction.type.toUpperCase()}: ${interaction.content}`,
|
|
containerTag: this.getContainerTag(customerId),
|
|
metadata: {
|
|
type: 'customer_interaction',
|
|
interactionType: interaction.type,
|
|
channel: interaction.channel,
|
|
outcome: interaction.outcome,
|
|
agentId: interaction.agentId,
|
|
timestamp: new Date().toISOString(),
|
|
...interaction.metadata
|
|
}
|
|
})
|
|
|
|
return result
|
|
} catch (error) {
|
|
console.error('Failed to add customer interaction:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async getCustomerHistory(customerId: string, limit: number = 10) {
|
|
try {
|
|
const memories = await client.documents.list({
|
|
containerTags: [this.getContainerTag(customerId)],
|
|
limit,
|
|
sort: 'updatedAt',
|
|
order: 'desc'
|
|
})
|
|
|
|
return memories.memories.map(memory => ({
|
|
id: memory.id,
|
|
content: memory.content,
|
|
type: memory.metadata?.interactionType || 'unknown',
|
|
channel: memory.metadata?.channel,
|
|
outcome: memory.metadata?.outcome,
|
|
timestamp: memory.metadata?.timestamp || memory.createdAt,
|
|
agentId: memory.metadata?.agentId
|
|
}))
|
|
} catch (error) {
|
|
console.error('Failed to get customer history:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async searchCustomerContext(customerId: string, query: string) {
|
|
try {
|
|
const results = await client.search.memories({
|
|
q: query,
|
|
containerTag: this.getContainerTag(customerId),
|
|
threshold: 0.6,
|
|
limit: 5,
|
|
rerank: true
|
|
})
|
|
|
|
return results.results.map(result => ({
|
|
content: result.memory,
|
|
similarity: result.similarity,
|
|
metadata: result.metadata
|
|
}))
|
|
} catch (error) {
|
|
console.error('Failed to search customer context:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async trackIssue(customerId: string, issue: {
|
|
subject: string
|
|
description: string
|
|
category: string
|
|
priority: 'low' | 'medium' | 'high' | 'urgent'
|
|
status: 'open' | 'pending' | 'resolved'
|
|
}) {
|
|
try {
|
|
const issueContent = `ISSUE: ${issue.subject}\n\nDescription: ${issue.description}\nCategory: ${issue.category}\nPriority: ${issue.priority}\nStatus: ${issue.status}`
|
|
|
|
const result = await client.add({
|
|
content: issueContent,
|
|
containerTag: this.getContainerTag(customerId),
|
|
metadata: {
|
|
type: 'support_issue',
|
|
subject: issue.subject,
|
|
category: issue.category,
|
|
priority: issue.priority,
|
|
status: issue.status,
|
|
createdAt: new Date().toISOString()
|
|
}
|
|
})
|
|
|
|
return result
|
|
} catch (error) {
|
|
console.error('Failed to track issue:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async updateIssueStatus(issueId: string, status: 'open' | 'pending' | 'resolved' | 'closed', resolution?: string) {
|
|
try {
|
|
// Note: In a real implementation, you'd update the memory
|
|
// For now, we'll add a status update
|
|
const memory = await client.documents.get(issueId)
|
|
const customerId = memory.containerTags?.[0]?.replace('customer_', '') || ''
|
|
|
|
const updateContent = `ISSUE UPDATE: ${memory.metadata?.subject}\nStatus changed to: ${status}${resolution ? `\nResolution: ${resolution}` : ''}`
|
|
|
|
return await this.addInteraction(customerId, {
|
|
type: 'ticket',
|
|
content: updateContent,
|
|
channel: 'internal',
|
|
outcome: status === 'resolved' ? 'resolved' : 'pending',
|
|
metadata: {
|
|
originalIssueId: issueId,
|
|
statusUpdate: true
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('Failed to update issue status:', error)
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
```
|
|
</Tab>
|
|
|
|
<Tab title="Python">
|
|
```python customer_context.py
|
|
from supermemory import Supermemory
|
|
import os
|
|
from typing import Dict, List, Any, Optional
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
class InteractionType(Enum):
|
|
CHAT = "chat"
|
|
EMAIL = "email"
|
|
PHONE = "phone"
|
|
TICKET = "ticket"
|
|
|
|
class Priority(Enum):
|
|
LOW = "low"
|
|
MEDIUM = "medium"
|
|
HIGH = "high"
|
|
URGENT = "urgent"
|
|
|
|
class Status(Enum):
|
|
OPEN = "open"
|
|
PENDING = "pending"
|
|
RESOLVED = "resolved"
|
|
CLOSED = "closed"
|
|
|
|
class CustomerContextManager:
|
|
def __init__(self):
|
|
self.client = Supermemory(api_key=os.getenv("SUPERMEMORY_API_KEY"))
|
|
|
|
def _get_container_tag(self, customer_id: str) -> str:
|
|
return f"customer_{customer_id}"
|
|
|
|
def add_interaction(self, customer_id: str, interaction: Dict[str, Any]) -> Dict:
|
|
"""Add a customer interaction to memory"""
|
|
try:
|
|
content = f"{interaction['type'].upper()}: {interaction['content']}"
|
|
|
|
result = self.client.add(
|
|
content=content,
|
|
container_tag=self._get_container_tag(customer_id),
|
|
metadata={
|
|
'type': 'customer_interaction',
|
|
'interactionType': interaction['type'],
|
|
'channel': interaction['channel'],
|
|
'outcome': interaction.get('outcome'),
|
|
'agentId': interaction.get('agentId'),
|
|
'timestamp': datetime.now().isoformat(),
|
|
**interaction.get('metadata', {})
|
|
}
|
|
)
|
|
return result
|
|
except Exception as e:
|
|
print(f"Failed to add customer interaction: {e}")
|
|
raise
|
|
|
|
def get_customer_history(self, customer_id: str, limit: int = 10) -> List[Dict]:
|
|
"""Get customer interaction history"""
|
|
try:
|
|
memories = self.client.documents.list(
|
|
container_tags=[self._get_container_tag(customer_id)],
|
|
limit=limit,
|
|
sort='updatedAt',
|
|
order='desc'
|
|
)
|
|
|
|
return [
|
|
{
|
|
'id': memory.id,
|
|
'content': memory.content,
|
|
'type': memory.metadata.get('interactionType', 'unknown') if memory.metadata else 'unknown',
|
|
'channel': memory.metadata.get('channel') if memory.metadata else None,
|
|
'outcome': memory.metadata.get('outcome') if memory.metadata else None,
|
|
'timestamp': memory.metadata.get('timestamp', memory.created_at) if memory.metadata else memory.created_at,
|
|
'agentId': memory.metadata.get('agentId') if memory.metadata else None
|
|
}
|
|
for memory in memories.memories
|
|
]
|
|
except Exception as e:
|
|
print(f"Failed to get customer history: {e}")
|
|
raise
|
|
|
|
def search_customer_context(self, customer_id: str, query: str) -> List[Dict]:
|
|
"""Search customer's interaction history"""
|
|
try:
|
|
results = self.client.search.memories(
|
|
q=query,
|
|
container_tag=self._get_container_tag(customer_id),
|
|
threshold=0.6,
|
|
limit=5,
|
|
rerank=True
|
|
)
|
|
|
|
return [
|
|
{
|
|
'content': result.memory,
|
|
'similarity': result.similarity,
|
|
'metadata': result.metadata
|
|
}
|
|
for result in results.results
|
|
]
|
|
except Exception as e:
|
|
print(f"Failed to search customer context: {e}")
|
|
raise
|
|
|
|
def track_issue(self, customer_id: str, issue: Dict[str, str]) -> Dict:
|
|
"""Track a customer support issue"""
|
|
try:
|
|
issue_content = f"""ISSUE: {issue['subject']}
|
|
|
|
Description: {issue['description']}
|
|
Category: {issue['category']}
|
|
Priority: {issue['priority']}
|
|
Status: {issue['status']}"""
|
|
|
|
result = self.client.add(
|
|
content=issue_content,
|
|
container_tag=self._get_container_tag(customer_id),
|
|
metadata={
|
|
'type': 'support_issue',
|
|
'subject': issue['subject'],
|
|
'category': issue['category'],
|
|
'priority': issue['priority'],
|
|
'status': issue['status'],
|
|
'createdAt': datetime.now().isoformat()
|
|
}
|
|
)
|
|
return result
|
|
except Exception as e:
|
|
print(f"Failed to track issue: {e}")
|
|
raise
|
|
|
|
def update_issue_status(self, issue_id: str, status: str, resolution: Optional[str] = None) -> Dict:
|
|
"""Update the status of a support issue"""
|
|
try:
|
|
# Get original issue
|
|
memory = self.client.documents.get(issue_id)
|
|
customer_id = (memory.container_tags[0] if memory.container_tags else '').replace('customer_', '')
|
|
|
|
update_content = f"ISSUE UPDATE: {memory.metadata.get('subject', 'Unknown')}\nStatus changed to: {status}"
|
|
if resolution:
|
|
update_content += f"\nResolution: {resolution}"
|
|
|
|
return self.add_interaction(customer_id, {
|
|
'type': 'ticket',
|
|
'content': update_content,
|
|
'channel': 'internal',
|
|
'outcome': 'resolved' if status == 'resolved' else 'pending',
|
|
'metadata': {
|
|
'originalIssueId': issue_id,
|
|
'statusUpdate': True
|
|
}
|
|
})
|
|
except Exception as e:
|
|
print(f"Failed to update issue status: {e}")
|
|
raise
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
### Step 2: Support API with Context
|
|
|
|
<Tabs>
|
|
<Tab title="Next.js API Route">
|
|
```typescript app/api/support/chat/route.ts
|
|
import { streamText } from 'ai'
|
|
import { createOpenAI } from '@ai-sdk/openai'
|
|
import { CustomerContextManager } from '@/lib/customer-context'
|
|
|
|
const openai = createOpenAI({
|
|
apiKey: process.env.OPENAI_API_KEY!
|
|
})
|
|
|
|
const contextManager = new CustomerContextManager()
|
|
|
|
interface Customer {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
tier: 'free' | 'pro' | 'enterprise'
|
|
joinDate: string
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
const {
|
|
message,
|
|
customerId,
|
|
customer,
|
|
conversationHistory = [],
|
|
agentId
|
|
} = await request.json()
|
|
|
|
try {
|
|
// Get customer history and context
|
|
const [history, contextResults] = await Promise.all([
|
|
contextManager.getCustomerHistory(customerId, 5),
|
|
contextManager.searchCustomerContext(customerId, message)
|
|
])
|
|
|
|
// Build customer context
|
|
const customerContext = `
|
|
CUSTOMER PROFILE:
|
|
- Name: ${customer.name}
|
|
- Email: ${customer.email}
|
|
- Tier: ${customer.tier.toUpperCase()}
|
|
- Member since: ${customer.joinDate}
|
|
|
|
RECENT INTERACTIONS (Last 5):
|
|
${history.map(h => `- ${h.timestamp}: ${h.type.toUpperCase()} - ${h.content.substring(0, 100)}...`).join('\n')}
|
|
|
|
RELEVANT CONTEXT:
|
|
${contextResults.map(c => `- ${c.content.substring(0, 150)}... (${(c.similarity * 100).toFixed(1)}% relevant)`).join('\n')}
|
|
`.trim()
|
|
|
|
// Determine if escalation is needed
|
|
const escalationKeywords = ['angry', 'frustrated', 'cancel', 'refund', 'legal', 'complaint', 'manager', 'supervisor']
|
|
const needsEscalation = escalationKeywords.some(keyword =>
|
|
message.toLowerCase().includes(keyword)
|
|
) || customer.tier === 'enterprise'
|
|
|
|
const systemPrompt = `You are a helpful customer support agent with access to complete customer history and context.
|
|
|
|
CUSTOMER CONTEXT:
|
|
${customerContext}
|
|
|
|
SUPPORT GUIDELINES:
|
|
1. **Personalization**: Address the customer by name and reference their tier/history when relevant
|
|
2. **Context Awareness**: Use previous interactions to inform your response
|
|
3. **Tier-Specific Service**:
|
|
- Free: Standard support, guide to self-service resources
|
|
- Pro: Priority support, detailed explanations, proactive suggestions
|
|
- Enterprise: White-glove service, immediate escalation path, dedicated attention
|
|
|
|
4. **Issue Tracking**: If this is a new issue, categorize it (billing, technical, account, product)
|
|
5. **Escalation**: ${needsEscalation ? 'This interaction may need human agent escalation - provide helpful response but prepare escalation summary' : 'Handle directly unless customer specifically requests human agent'}
|
|
|
|
RESPONSE STYLE:
|
|
- Professional but friendly
|
|
- Reference specific details from customer history when relevant
|
|
- Provide actionable next steps
|
|
- Include relevant links or resources for their tier level
|
|
|
|
If you cannot resolve the issue completely, prepare a clear summary for escalation to human agents.`
|
|
|
|
const messages = [
|
|
{ role: 'system' as const, content: systemPrompt },
|
|
...conversationHistory,
|
|
{ role: 'user' as const, content: message }
|
|
]
|
|
|
|
const result = await streamText({
|
|
model: openai('gpt-5'),
|
|
messages,
|
|
temperature: 0.3,
|
|
maxTokens: 800,
|
|
onFinish: async (completion) => {
|
|
// Store this interaction
|
|
await contextManager.addInteraction(customerId, {
|
|
type: 'chat',
|
|
content: `Customer: ${message}\nAgent: ${completion.text}`,
|
|
channel: 'web_chat',
|
|
outcome: needsEscalation ? 'escalated' : 'resolved',
|
|
agentId,
|
|
metadata: {
|
|
customerTier: customer.tier,
|
|
needsEscalation,
|
|
responseLength: completion.text.length
|
|
}
|
|
})
|
|
|
|
// If this looks like a new issue, track it
|
|
if (message.length > 50 && !contextResults.some(c => c.similarity > 0.8)) {
|
|
const issueCategory = categorizeIssue(message)
|
|
const priority = determinePriority(customer.tier, message)
|
|
|
|
await contextManager.trackIssue(customerId, {
|
|
subject: message.substring(0, 100),
|
|
description: message,
|
|
category: issueCategory,
|
|
priority,
|
|
status: needsEscalation ? 'pending' : 'open'
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
return result.toAIStreamResponse({
|
|
data: {
|
|
needsEscalation,
|
|
customerTier: customer.tier,
|
|
contextCount: contextResults.length
|
|
}
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('Support chat error:', error)
|
|
return Response.json(
|
|
{ error: 'Failed to process support request', details: error.message },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
function categorizeIssue(message: string): string {
|
|
const categories = {
|
|
billing: ['bill', 'charge', 'payment', 'refund', 'price', 'cost'],
|
|
technical: ['error', 'bug', 'broken', 'not working', 'crash', 'slow'],
|
|
account: ['login', 'password', 'access', 'settings', 'profile'],
|
|
product: ['feature', 'how to', 'tutorial', 'help', 'guide']
|
|
}
|
|
|
|
const messageLower = message.toLowerCase()
|
|
|
|
for (const [category, keywords] of Object.entries(categories)) {
|
|
if (keywords.some(keyword => messageLower.includes(keyword))) {
|
|
return category
|
|
}
|
|
}
|
|
|
|
return 'general'
|
|
}
|
|
|
|
function determinePriority(tier: string, message: string): 'low' | 'medium' | 'high' | 'urgent' {
|
|
const urgentKeywords = ['urgent', 'critical', 'emergency', 'down', 'broken']
|
|
const highKeywords = ['important', 'asap', 'soon', 'problem']
|
|
|
|
const messageLower = message.toLowerCase()
|
|
|
|
if (urgentKeywords.some(keyword => messageLower.includes(keyword))) {
|
|
return 'urgent'
|
|
}
|
|
|
|
if (tier === 'enterprise') {
|
|
return highKeywords.some(keyword => messageLower.includes(keyword)) ? 'urgent' : 'high'
|
|
}
|
|
|
|
if (tier === 'pro') {
|
|
return highKeywords.some(keyword => messageLower.includes(keyword)) ? 'high' : 'medium'
|
|
}
|
|
|
|
return 'low'
|
|
}
|
|
```
|
|
</Tab>
|
|
|
|
<Tab title="Python FastAPI">
|
|
```python support_api.py
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
from typing import List, Dict, Any, Optional
|
|
import openai
|
|
from customer_context import CustomerContextManager
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
app = FastAPI()
|
|
|
|
openai_client = openai.AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
|
context_manager = CustomerContextManager()
|
|
|
|
class Customer(BaseModel):
|
|
id: str
|
|
name: str
|
|
email: str
|
|
tier: str
|
|
joinDate: str
|
|
|
|
class SupportRequest(BaseModel):
|
|
message: str
|
|
customerId: str
|
|
customer: Customer
|
|
conversationHistory: List[Dict[str, str]] = []
|
|
agentId: Optional[str] = None
|
|
|
|
def categorize_issue(message: str) -> str:
|
|
"""Categorize support issue based on message content"""
|
|
categories = {
|
|
'billing': ['bill', 'charge', 'payment', 'refund', 'price', 'cost'],
|
|
'technical': ['error', 'bug', 'broken', 'not working', 'crash', 'slow'],
|
|
'account': ['login', 'password', 'access', 'settings', 'profile'],
|
|
'product': ['feature', 'how to', 'tutorial', 'help', 'guide']
|
|
}
|
|
|
|
message_lower = message.lower()
|
|
|
|
for category, keywords in categories.items():
|
|
if any(keyword in message_lower for keyword in keywords):
|
|
return category
|
|
|
|
return 'general'
|
|
|
|
def determine_priority(tier: str, message: str) -> str:
|
|
"""Determine issue priority based on tier and message content"""
|
|
urgent_keywords = ['urgent', 'critical', 'emergency', 'down', 'broken']
|
|
high_keywords = ['important', 'asap', 'soon', 'problem']
|
|
|
|
message_lower = message.lower()
|
|
|
|
if any(keyword in message_lower for keyword in urgent_keywords):
|
|
return 'urgent'
|
|
|
|
if tier == 'enterprise':
|
|
return 'urgent' if any(keyword in message_lower for keyword in high_keywords) else 'high'
|
|
|
|
if tier == 'pro':
|
|
return 'high' if any(keyword in message_lower for keyword in high_keywords) else 'medium'
|
|
|
|
return 'low'
|
|
|
|
@app.post("/support/chat")
|
|
async def support_chat(request: SupportRequest):
|
|
try:
|
|
# Get customer history and context
|
|
history = context_manager.get_customer_history(request.customerId, 5)
|
|
context_results = context_manager.search_customer_context(request.customerId, request.message)
|
|
|
|
# Build customer context
|
|
customer_context = f"""
|
|
CUSTOMER PROFILE:
|
|
- Name: {request.customer.name}
|
|
- Email: {request.customer.email}
|
|
- Tier: {request.customer.tier.upper()}
|
|
- Member since: {request.customer.joinDate}
|
|
|
|
RECENT INTERACTIONS (Last 5):
|
|
{chr(10).join([f"- {h['timestamp']}: {h['type'].upper()} - {h['content'][:100]}..." for h in history])}
|
|
|
|
RELEVANT CONTEXT:
|
|
{chr(10).join([f"- {c['content'][:150]}... ({c['similarity']*100:.1f}% relevant)" for c in context_results])}
|
|
""".strip()
|
|
|
|
# Determine if escalation is needed
|
|
escalation_keywords = ['angry', 'frustrated', 'cancel', 'refund', 'legal', 'complaint', 'manager', 'supervisor']
|
|
needs_escalation = any(keyword in request.message.lower() for keyword in escalation_keywords) or request.customer.tier == 'enterprise'
|
|
|
|
system_prompt = f"""You are a helpful customer support agent with access to complete customer history and context.
|
|
|
|
CUSTOMER CONTEXT:
|
|
{customer_context}
|
|
|
|
SUPPORT GUIDELINES:
|
|
1. **Personalization**: Address the customer by name and reference their tier/history when relevant
|
|
2. **Context Awareness**: Use previous interactions to inform your response
|
|
3. **Tier-Specific Service**:
|
|
- Free: Standard support, guide to self-service resources
|
|
- Pro: Priority support, detailed explanations, proactive suggestions
|
|
- Enterprise: White-glove service, immediate escalation path, dedicated attention
|
|
|
|
4. **Issue Tracking**: If this is a new issue, categorize it (billing, technical, account, product)
|
|
5. **Escalation**: {'This interaction may need human agent escalation - provide helpful response but prepare escalation summary' if needs_escalation else 'Handle directly unless customer specifically requests human agent'}
|
|
|
|
RESPONSE STYLE:
|
|
- Professional but friendly
|
|
- Reference specific details from customer history when relevant
|
|
- Provide actionable next steps
|
|
- Include relevant links or resources for their tier level
|
|
|
|
If you cannot resolve the issue completely, prepare a clear summary for escalation to human agents."""
|
|
|
|
messages = [
|
|
{"role": "system", "content": system_prompt},
|
|
*request.conversationHistory,
|
|
{"role": "user", "content": request.message}
|
|
]
|
|
|
|
response = await openai_client.chat.completions.create(
|
|
model="gpt-5",
|
|
messages=messages,
|
|
temperature=0.3,
|
|
max_tokens=800,
|
|
stream=True
|
|
)
|
|
|
|
async def generate():
|
|
full_response = ""
|
|
async for chunk in response:
|
|
if chunk.choices[0].delta.content:
|
|
content = chunk.choices[0].delta.content
|
|
full_response += content
|
|
yield f"data: {json.dumps({'content': content})}\n\n"
|
|
|
|
# Store interaction after completion
|
|
context_manager.add_interaction(request.customerId, {
|
|
'type': 'chat',
|
|
'content': f"Customer: {request.message}\nAgent: {full_response}",
|
|
'channel': 'web_chat',
|
|
'outcome': 'escalated' if needs_escalation else 'resolved',
|
|
'agentId': request.agentId,
|
|
'metadata': {
|
|
'customerTier': request.customer.tier,
|
|
'needsEscalation': needs_escalation,
|
|
'responseLength': len(full_response)
|
|
}
|
|
})
|
|
|
|
# Track new issues
|
|
if len(request.message) > 50 and not any(c['similarity'] > 0.8 for c in context_results):
|
|
issue_category = categorize_issue(request.message)
|
|
priority = determine_priority(request.customer.tier, request.message)
|
|
|
|
context_manager.track_issue(request.customerId, {
|
|
'subject': request.message[:100],
|
|
'description': request.message,
|
|
'category': issue_category,
|
|
'priority': priority,
|
|
'status': 'pending' if needs_escalation else 'open'
|
|
})
|
|
|
|
yield f"data: {json.dumps({'done': True, 'needsEscalation': needs_escalation})}\n\n"
|
|
|
|
return StreamingResponse(generate(), media_type="text/plain")
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Support chat error: {str(e)}")
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
### Step 3: Support Dashboard Interface
|
|
|
|
```tsx app/support/page.tsx
|
|
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useChat } from 'ai/react'
|
|
import { CustomerContextManager } from '@/lib/customer-context'
|
|
|
|
interface Customer {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
tier: 'free' | 'pro' | 'enterprise'
|
|
joinDate: string
|
|
}
|
|
|
|
interface SupportTicket {
|
|
id: string
|
|
subject: string
|
|
status: 'open' | 'pending' | 'resolved' | 'closed'
|
|
priority: 'low' | 'medium' | 'high' | 'urgent'
|
|
category: string
|
|
createdAt: string
|
|
}
|
|
|
|
export default function SupportDashboard() {
|
|
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null)
|
|
const [customerHistory, setCustomerHistory] = useState<any[]>([])
|
|
const [tickets, setTickets] = useState<SupportTicket[]>([])
|
|
const [showEscalation, setShowEscalation] = useState(false)
|
|
const [agentId] = useState('agent_001') // In real app, get from auth
|
|
|
|
const contextManager = new CustomerContextManager()
|
|
|
|
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
|
|
api: '/api/support/chat',
|
|
body: {
|
|
customerId: selectedCustomer?.id,
|
|
customer: selectedCustomer,
|
|
agentId
|
|
},
|
|
onFinish: (message, { data }) => {
|
|
if (data?.needsEscalation) {
|
|
setShowEscalation(true)
|
|
}
|
|
// Refresh customer history
|
|
if (selectedCustomer) {
|
|
loadCustomerHistory(selectedCustomer.id)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Mock customers - in real app, fetch from your customer database
|
|
const mockCustomers: Customer[] = [
|
|
{
|
|
id: 'cust_001',
|
|
name: 'Sarah Johnson',
|
|
email: 'sarah@example.com',
|
|
tier: 'pro',
|
|
joinDate: '2023-06-15'
|
|
},
|
|
{
|
|
id: 'cust_002',
|
|
name: 'TechCorp Inc',
|
|
email: 'support@techcorp.com',
|
|
tier: 'enterprise',
|
|
joinDate: '2022-03-20'
|
|
},
|
|
{
|
|
id: 'cust_003',
|
|
name: 'Mike Chen',
|
|
email: 'mike@startup.com',
|
|
tier: 'free',
|
|
joinDate: '2024-01-10'
|
|
}
|
|
]
|
|
|
|
const loadCustomerHistory = async (customerId: string) => {
|
|
try {
|
|
const history = await contextManager.getCustomerHistory(customerId, 10)
|
|
setCustomerHistory(history)
|
|
} catch (error) {
|
|
console.error('Failed to load customer history:', error)
|
|
}
|
|
}
|
|
|
|
const handleCustomerSelect = async (customer: Customer) => {
|
|
setSelectedCustomer(customer)
|
|
await loadCustomerHistory(customer.id)
|
|
setShowEscalation(false)
|
|
}
|
|
|
|
const getTierColor = (tier: string) => {
|
|
switch (tier) {
|
|
case 'enterprise': return 'bg-purple-100 text-purple-800'
|
|
case 'pro': return 'bg-blue-100 text-blue-800'
|
|
case 'free': return 'bg-gray-100 text-gray-800'
|
|
default: return 'bg-gray-100 text-gray-800'
|
|
}
|
|
}
|
|
|
|
const getPriorityColor = (priority: string) => {
|
|
switch (priority) {
|
|
case 'urgent': return 'bg-red-100 text-red-800'
|
|
case 'high': return 'bg-orange-100 text-orange-800'
|
|
case 'medium': return 'bg-yellow-100 text-yellow-800'
|
|
case 'low': return 'bg-green-100 text-green-800'
|
|
default: return 'bg-gray-100 text-gray-800'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="h-screen flex">
|
|
{/* Customer List Sidebar */}
|
|
<div className="w-80 bg-white border-r border-gray-200 overflow-y-auto">
|
|
<div className="p-4 border-b">
|
|
<h2 className="text-lg font-semibold">Customers</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-200">
|
|
{mockCustomers.map((customer) => (
|
|
<div
|
|
key={customer.id}
|
|
onClick={() => handleCustomerSelect(customer)}
|
|
className={`p-4 cursor-pointer hover:bg-gray-50 ${
|
|
selectedCustomer?.id === customer.id ? 'bg-blue-50 border-r-2 border-blue-500' : ''
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="font-medium text-gray-900">{customer.name}</div>
|
|
<span className={`px-2 py-1 text-xs rounded-full ${getTierColor(customer.tier)}`}>
|
|
{customer.tier}
|
|
</span>
|
|
</div>
|
|
<div className="text-sm text-gray-600">{customer.email}</div>
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
Member since {customer.joinDate}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 flex flex-col">
|
|
{selectedCustomer ? (
|
|
<>
|
|
{/* Customer Header */}
|
|
<div className="bg-white border-b border-gray-200 p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-xl font-semibold">{selectedCustomer.name}</h1>
|
|
<p className="text-gray-600">{selectedCustomer.email}</p>
|
|
</div>
|
|
<div className="flex items-center space-x-4">
|
|
<span className={`px-3 py-1 text-sm rounded-full ${getTierColor(selectedCustomer.tier)}`}>
|
|
{selectedCustomer.tier.toUpperCase()} Customer
|
|
</span>
|
|
{showEscalation && (
|
|
<div className="bg-red-100 text-red-800 px-3 py-1 text-sm rounded-full">
|
|
Needs Escalation
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 flex">
|
|
{/* Chat Area */}
|
|
<div className="flex-1 flex flex-col">
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{messages.length === 0 && (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<div className="text-lg font-medium">Welcome to Support Chat</div>
|
|
<p className="mt-2">
|
|
Start a conversation with {selectedCustomer.name}
|
|
</p>
|
|
<div className="mt-4 text-sm">
|
|
<p><strong>Customer Tier:</strong> {selectedCustomer.tier}</p>
|
|
<p><strong>Join Date:</strong> {selectedCustomer.joinDate}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{messages.map((message) => (
|
|
<div
|
|
key={message.id}
|
|
className={`flex ${
|
|
message.role === 'user' ? 'justify-end' : 'justify-start'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`max-w-2xl p-4 rounded-lg ${
|
|
message.role === 'user'
|
|
? 'bg-blue-500 text-white'
|
|
: 'bg-gray-100 text-gray-900'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-2 mb-2">
|
|
<span className="text-sm font-medium">
|
|
{message.role === 'user' ? selectedCustomer.name : 'Support Agent'}
|
|
</span>
|
|
<span className="text-xs opacity-75">
|
|
{new Date().toLocaleTimeString()}
|
|
</span>
|
|
</div>
|
|
<div className="whitespace-pre-wrap">{message.content}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isLoading && (
|
|
<div className="flex justify-start">
|
|
<div className="max-w-2xl p-4 bg-gray-100 rounded-lg">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
|
<span className="text-sm">Agent is typing...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Chat Input */}
|
|
<div className="border-t border-gray-200 p-4">
|
|
<form onSubmit={handleSubmit} className="flex space-x-2">
|
|
<input
|
|
value={input}
|
|
onChange={handleInputChange}
|
|
placeholder={`Respond to ${selectedCustomer.name}...`}
|
|
className="flex-1 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
disabled={isLoading}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || !input.trim()}
|
|
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Send
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customer History Sidebar */}
|
|
<div className="w-80 bg-gray-50 border-l border-gray-200 overflow-y-auto">
|
|
<div className="p-4 border-b bg-white">
|
|
<h3 className="font-medium">Customer History</h3>
|
|
</div>
|
|
<div className="p-4 space-y-3">
|
|
{customerHistory.map((interaction, index) => (
|
|
<div key={index} className="bg-white p-3 rounded-lg border text-sm">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-medium capitalize">{interaction.type}</span>
|
|
<span className="text-xs text-gray-500">
|
|
{new Date(interaction.timestamp).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
<p className="text-gray-700 line-clamp-3">
|
|
{interaction.content.length > 100
|
|
? `${interaction.content.substring(0, 100)}...`
|
|
: interaction.content
|
|
}
|
|
</p>
|
|
{interaction.outcome && (
|
|
<div className="mt-2">
|
|
<span className={`text-xs px-2 py-1 rounded ${
|
|
interaction.outcome === 'resolved'
|
|
? 'bg-green-100 text-green-800'
|
|
: interaction.outcome === 'escalated'
|
|
? 'bg-red-100 text-red-800'
|
|
: 'bg-yellow-100 text-yellow-800'
|
|
}`}>
|
|
{interaction.outcome}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{customerHistory.length === 0 && (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<p>No previous interactions</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex-1 flex items-center justify-center text-gray-500">
|
|
<div className="text-center">
|
|
<div className="text-lg font-medium">Customer Support</div>
|
|
<p className="mt-2">Select a customer to start a support conversation</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Testing Your Support System
|
|
|
|
### Step 4: Test Support Scenarios
|
|
|
|
1. **Test Customer Tiers**:
|
|
- Free tier: Basic responses, self-service guidance
|
|
- Pro tier: Detailed help, proactive suggestions
|
|
- Enterprise: White-glove service, escalation readiness
|
|
|
|
2. **Test Memory & Context**:
|
|
- Ask about a previous issue
|
|
- Reference customer preferences
|
|
- Follow up on unresolved tickets
|
|
|
|
3. **Test Escalation Triggers**:
|
|
- Use keywords like "angry", "manager", "refund"
|
|
- Test enterprise customer automatic escalation
|
|
|
|
This comprehensive customer support recipe provides the foundation for building intelligent, context-aware support systems that improve customer satisfaction through personalized service.
|
|
|
|
---
|
|
|
|
*Customize this recipe based on your specific support workflows and customer needs.*
|