Skip to content

Latest commit

 

History

History
657 lines (531 loc) · 19.1 KB

File metadata and controls

657 lines (531 loc) · 19.1 KB
layout default
title Chapter 4: Conversation Management
parent Chatbox Tutorial
nav_order 4

Chapter 4: Conversation Management

Welcome to Chapter 4: Conversation Management. In this part of Chatbox Tutorial: Building Modern AI Chat Interfaces, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.

This chapter covers managing chat conversations, including history, context, and multi-conversation workflows.

💬 Conversation Architecture

Conversation Data Structure

// Comprehensive conversation model
interface Conversation {
  id: string
  title: string
  createdAt: Date
  updatedAt: Date
  messages: Message[]
  metadata: {
    provider: string
    model: string
    totalTokens: number
    estimatedCost: number
    tags: string[]
    isArchived: boolean
    isFavorite: boolean
  }
  context: {
    summary: string
    keyTopics: string[]
    participants: string[]
    sentiment: 'positive' | 'neutral' | 'negative'
  }
}

interface Message {
  id: string
  role: 'user' | 'assistant' | 'system'
  content: string
  timestamp: Date
  metadata: {
    tokens: number
    model: string
    processingTime: number
    attachments?: Attachment[]
  }
}

Conversation Manager

// Central conversation management
class ConversationManager {
  private conversations: Map<string, Conversation> = new Map()
  private activeConversation: string | null = null

  async createConversation(title?: string): Promise<string> {
    const id = this.generateId()
    const conversation: Conversation = {
      id,
      title: title || 'New Conversation',
      createdAt: new Date(),
      updatedAt: new Date(),
      messages: [],
      metadata: {
        provider: 'openai',
        model: 'gpt-3.5-turbo',
        totalTokens: 0,
        estimatedCost: 0,
        tags: [],
        isArchived: false,
        isFavorite: false
      },
      context: {
        summary: '',
        keyTopics: [],
        participants: ['user'],
        sentiment: 'neutral'
      }
    }

    this.conversations.set(id, conversation)
    this.activeConversation = id

    await this.persistConversation(conversation)
    return id
  }

  async addMessage(conversationId: string, message: Omit<Message, 'id' | 'timestamp'>): Promise<void> {
    const conversation = this.conversations.get(conversationId)
    if (!conversation) {
      throw new Error('Conversation not found')
    }

    const fullMessage: Message = {
      ...message,
      id: this.generateId(),
      timestamp: new Date()
    }

    conversation.messages.push(fullMessage)
    conversation.updatedAt = new Date()

    // Update conversation metadata
    this.updateConversationMetadata(conversation, fullMessage)

    await this.persistConversation(conversation)
  }

  async getConversation(conversationId: string): Promise<Conversation | null> {
    return this.conversations.get(conversationId) || null
  }

  async listConversations(filters?: ConversationFilters): Promise<Conversation[]> {
    let conversations = Array.from(this.conversations.values())

    if (filters) {
      conversations = this.applyFilters(conversations, filters)
    }

    return conversations.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
  }

  private updateConversationMetadata(conversation: Conversation, message: Message) {
    conversation.metadata.totalTokens += message.metadata.tokens || 0
    conversation.metadata.estimatedCost = this.calculateCost(conversation)

    // Update title if it's generic and we have content
    if (conversation.title === 'New Conversation' && conversation.messages.length > 1) {
      conversation.title = this.generateTitle(conversation)
    }

    // Update context
    conversation.context = this.updateContext(conversation, message)
  }

  private generateTitle(conversation: Conversation): string {
    const firstUserMessage = conversation.messages.find(m => m.role === 'user')
    if (firstUserMessage) {
      return firstUserMessage.content.slice(0, 50) + (firstUserMessage.content.length > 50 ? '...' : '')
    }
    return 'Conversation'
  }

  private updateContext(conversation: Conversation, message: Message): Conversation['context'] {
    // Extract key topics from messages
    const topics = this.extractTopics(conversation.messages)

    // Generate conversation summary
    const summary = this.generateSummary(conversation.messages)

    // Analyze sentiment
    const sentiment = this.analyzeSentiment(conversation.messages)

    return {
      summary,
      keyTopics: topics,
      participants: ['user', 'assistant'],
      sentiment
    }
  }
}

📚 Conversation History

Message Storage

// Efficient message storage and retrieval
class MessageStore {
  private storage: MessageStorage
  private cache: Map<string, Message[]> = new Map()

  async storeMessages(conversationId: string, messages: Message[]): Promise<void> {
    await this.storage.store(conversationId, messages)
    this.cache.set(conversationId, messages)
  }

  async getMessages(conversationId: string, options?: MessageQueryOptions): Promise<Message[]> {
    // Check cache first
    if (this.cache.has(conversationId)) {
      return this.applyOptions(this.cache.get(conversationId)!, options)
    }

    // Load from storage
    const messages = await this.storage.retrieve(conversationId)
    this.cache.set(conversationId, messages)

    return this.applyOptions(messages, options)
  }

  async searchMessages(conversationId: string, query: string): Promise<Message[]> {
    const messages = await this.getMessages(conversationId)
    const queryTerms = query.toLowerCase().split(' ')

    return messages.filter(message =>
      queryTerms.some(term =>
        message.content.toLowerCase().includes(term)
      )
    )
  }

  private applyOptions(messages: Message[], options?: MessageQueryOptions): Message[] {
    if (!options) return messages

    let result = messages

    // Apply limit
    if (options.limit) {
      result = result.slice(-options.limit)
    }

    // Apply date range
    if (options.startDate) {
      result = result.filter(m => m.timestamp >= options.startDate!)
    }

    if (options.endDate) {
      result = result.filter(m => m.timestamp <= options.endDate!)
    }

    // Apply role filter
    if (options.role) {
      result = result.filter(m => m.role === options.role)
    }

    return result
  }
}

Conversation Search

// Advanced conversation search
class ConversationSearch {
  private conversations: Conversation[]
  private searchIndex: SearchIndex

  async search(query: string, filters?: SearchFilters): Promise<SearchResult[]> {
    // Search across all conversations
    const results = await this.searchIndex.search(query)

    // Apply filters
    let filteredResults = this.applyFilters(results, filters)

    // Sort by relevance
    filteredResults.sort((a, b) => b.score - a.score)

    return filteredResults
  }

  async searchWithinConversation(conversationId: string, query: string): Promise<Message[]> {
    const conversation = await this.conversationManager.getConversation(conversationId)
    if (!conversation) return []

    return conversation.messages.filter(message =>
      message.content.toLowerCase().includes(query.toLowerCase())
    )
  }

  private applyFilters(results: SearchResult[], filters?: SearchFilters): SearchResult[] {
    if (!filters) return results

    return results.filter(result => {
      // Date range filter
      if (filters.startDate && result.timestamp < filters.startDate) return false
      if (filters.endDate && result.timestamp > filters.endDate) return false

      // Conversation filter
      if (filters.conversationId && result.conversationId !== filters.conversationId) return false

      // Provider filter
      if (filters.provider && result.provider !== filters.provider) return false

      return true
    })
  }

  async buildSearchIndex(): Promise<void> {
    const conversations = await this.conversationManager.listConversations()

    for (const conversation of conversations) {
      for (const message of conversation.messages) {
        await this.searchIndex.index({
          id: message.id,
          content: message.content,
          conversationId: conversation.id,
          timestamp: message.timestamp,
          provider: conversation.metadata.provider,
          score: 1.0
        })
      }
    }
  }
}

🧠 Context Management

Conversation Context

// Intelligent context handling
class ContextManager {
  private contextWindow = 10
  private maxTokens = 4000

  buildContext(conversation: Conversation, currentMessage?: string): Context {
    // Get recent messages
    const recentMessages = conversation.messages.slice(-this.contextWindow)

    // Build context string
    const contextMessages = recentMessages.map(msg => ({
      role: msg.role,
      content: msg.content
    }))

    // Add current message if provided
    if (currentMessage) {
      contextMessages.push({
        role: 'user',
        content: currentMessage
      })
    }

    // Estimate token count
    const tokenCount = this.estimateTokens(contextMessages)

    // Trim if necessary
    if (tokenCount > this.maxTokens) {
      contextMessages.splice(0, Math.floor(contextMessages.length * 0.2))
    }

    return {
      messages: contextMessages,
      tokenCount: this.estimateTokens(contextMessages),
      summary: this.generateSummary(conversation),
      metadata: {
        conversationId: conversation.id,
        messageCount: contextMessages.length,
        timeRange: this.getTimeRange(recentMessages)
      }
    }
  }

  private estimateTokens(messages: any[]): number {
    // Rough estimation: 1 token ≈ 4 characters
    return messages.reduce((total, msg) =>
      total + Math.ceil(msg.content.length / 4), 0
    )
  }

  private generateSummary(conversation: Conversation): string {
    if (conversation.messages.length === 0) return ''

    const firstMessage = conversation.messages[0].content
    const recentMessages = conversation.messages.slice(-3)

    return `Conversation started with: "${firstMessage.slice(0, 100)}..."`
  }

  private getTimeRange(messages: Message[]): { start: Date; end: Date } {
    if (messages.length === 0) {
      const now = new Date()
      return { start: now, end: now }
    }

    return {
      start: messages[0].timestamp,
      end: messages[messages.length - 1].timestamp
    }
  }
}

📊 Conversation Analytics

Usage Statistics

// Conversation usage analytics
class ConversationAnalytics {
  private stats: Map<string, ConversationStats> = new Map()

  trackConversationActivity(conversationId: string, activity: ConversationActivity) {
    const stats = this.stats.get(conversationId) || this.initializeStats()

    switch (activity.type) {
      case 'message_sent':
        stats.totalMessages++
        stats.lastActivity = new Date()
        break
      case 'message_received':
        stats.totalMessages++
        stats.averageResponseTime =
          (stats.averageResponseTime + activity.responseTime) / 2
        break
      case 'search_performed':
        stats.totalSearches++
        break
    }

    this.stats.set(conversationId, stats)
  }

  getConversationStats(conversationId: string): ConversationStats | null {
    return this.stats.get(conversationId) || null
  }

  getGlobalStats(): GlobalStats {
    const allStats = Array.from(this.stats.values())

    return {
      totalConversations: allStats.length,
      totalMessages: allStats.reduce((sum, stats) => sum + stats.totalMessages, 0),
      averageMessagesPerConversation: allStats.reduce((sum, stats) => sum + stats.totalMessages, 0) / allStats.length,
      mostActiveConversation: this.findMostActiveConversation()
    }
  }

  private initializeStats(): ConversationStats {
    return {
      totalMessages: 0,
      totalSearches: 0,
      averageResponseTime: 0,
      createdAt: new Date(),
      lastActivity: new Date()
    }
  }

  private findMostActiveConversation(): string {
    let mostActive = ''
    let maxMessages = 0

    for (const [id, stats] of this.stats) {
      if (stats.totalMessages > maxMessages) {
        maxMessages = stats.totalMessages
        mostActive = id
      }
    }

    return mostActive
  }
}

🔄 Conversation Operations

Import/Export

// Conversation import/export functionality
class ConversationImportExport {
  async exportConversation(conversationId: string, format: 'json' | 'markdown' = 'json'): Promise<string> {
    const conversation = await this.conversationManager.getConversation(conversationId)
    if (!conversation) {
      throw new Error('Conversation not found')
    }

    switch (format) {
      case 'json':
        return JSON.stringify(conversation, null, 2)
      case 'markdown':
        return this.convertToMarkdown(conversation)
      default:
        throw new Error(`Unsupported format: ${format}`)
    }
  }

  async importConversation(data: string, format: 'json' | 'markdown' = 'json'): Promise<string> {
    let conversation: Conversation

    switch (format) {
      case 'json':
        conversation = JSON.parse(data)
        break
      case 'markdown':
        conversation = this.parseFromMarkdown(data)
        break
      default:
        throw new Error(`Unsupported format: ${format}`)
    }

    // Validate and clean imported data
    conversation.id = this.generateId()
    conversation.createdAt = new Date()
    conversation.updatedAt = new Date()

    await this.conversationManager.saveConversation(conversation)
    return conversation.id
  }

  private convertToMarkdown(conversation: Conversation): string {
    let markdown = `# ${conversation.title}\n\n`
    markdown += `Created: ${conversation.createdAt.toISOString()}\n\n`

    for (const message of conversation.messages) {
      const role = message.role === 'user' ? 'User' : 'Assistant'
      markdown += `**${role}:** ${message.content}\n\n`
    }

    return markdown
  }

  private parseFromMarkdown(markdown: string): Conversation {
    const lines = markdown.split('\n')
    const title = lines[0].replace('# ', '')
    const messages: Message[] = []

    let currentRole: 'user' | 'assistant' | null = null
    let currentContent = ''

    for (let i = 2; i < lines.length; i++) {
      const line = lines[i]

      if (line.startsWith('**User:**')) {
        if (currentRole) {
          messages.push(this.createMessage(currentRole, currentContent.trim()))
        }
        currentRole = 'user'
        currentContent = line.replace('**User:**', '').trim()
      } else if (line.startsWith('**Assistant:**')) {
        if (currentRole) {
          messages.push(this.createMessage(currentRole, currentContent.trim()))
        }
        currentRole = 'assistant'
        currentContent = line.replace('**Assistant:**', '').trim()
      } else if (line.trim() === '') {
        // Empty line, process current message
        if (currentRole && currentContent) {
          messages.push(this.createMessage(currentRole, currentContent.trim()))
          currentRole = null
          currentContent = ''
        }
      } else if (currentRole) {
        currentContent += ' ' + line.trim()
      }
    }

    // Add final message
    if (currentRole && currentContent) {
      messages.push(this.createMessage(currentRole, currentContent.trim()))
    }

    return {
      id: '',
      title,
      createdAt: new Date(),
      updatedAt: new Date(),
      messages,
      metadata: {
        provider: 'imported',
        model: 'unknown',
        totalTokens: 0,
        estimatedCost: 0,
        tags: ['imported'],
        isArchived: false,
        isFavorite: false
      },
      context: {
        summary: `Imported conversation: ${title}`,
        keyTopics: [],
        participants: ['user', 'assistant'],
        sentiment: 'neutral'
      }
    }
  }

  private createMessage(role: 'user' | 'assistant', content: string): Message {
    return {
      id: this.generateId(),
      role,
      content,
      timestamp: new Date(),
      metadata: {
        tokens: Math.ceil(content.length / 4),
        model: 'imported',
        processingTime: 0
      }
    }
  }
}

📝 Chapter Summary

  • ✅ Built comprehensive conversation architecture
  • ✅ Implemented efficient message storage
  • ✅ Created advanced conversation search
  • ✅ Developed intelligent context management
  • ✅ Added conversation analytics
  • ✅ Built import/export functionality

Key Takeaways:

  • Conversation management requires careful data structure design
  • Efficient storage and retrieval are crucial for performance
  • Context management enables coherent conversations
  • Search capabilities enhance user experience
  • Analytics provide insights for improvement
  • Import/export enables data portability

What Problem Does This Solve?

Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for conversation, messages, conversationId so behavior stays predictable as complexity grows.

In practical terms, this chapter helps you avoid three common failures:

  • coupling core logic too tightly to one implementation path
  • missing the handoff boundaries between setup, execution, and validation
  • shipping changes without clear rollback or observability strategy

After working through this chapter, you should be able to reason about Chapter 4: Conversation Management as an operating subsystem inside Chatbox Tutorial: Building Modern AI Chat Interfaces, with explicit contracts for inputs, state transitions, and outputs.

Use the implementation notes around Conversation, message, stats as your checklist when adapting these patterns to your own repository.

How it Works Under the Hood

Under the hood, Chapter 4: Conversation Management usually follows a repeatable control path:

  1. Context bootstrap: initialize runtime config and prerequisites for conversation.
  2. Input normalization: shape incoming data so messages receives stable contracts.
  3. Core execution: run the main logic branch and propagate intermediate state through conversationId.
  4. Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
  5. Output composition: return canonical result payloads for downstream consumers.
  6. Operational telemetry: emit logs/metrics needed for debugging and performance tuning.

When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.

Source Walkthrough

Use the following upstream sources to verify implementation details while reading this chapter:

  • View Repo Why it matters: authoritative reference on View Repo (github.com).

Suggested trace strategy:

  • search upstream code for conversation and messages to map concrete implementation paths
  • compare docs claims against actual runtime/config code before reusing patterns in production

Chapter Connections