Skip to main content

Best Practices

This guide provides comprehensive best practices for integrating with the Blue Oyster Personal Companion API, covering performance, reliability, security, and user experience considerations.

Conversation Management

Thread Continuity

Always use thread IDs for conversation continuity and context preservation.
class ConversationManager {
  constructor() {
    this.activeThreads = new Map();
  }

  async startConversation(userId, initialMessage, personality = 'friend') {
    // Create a new thread
    const thread = await this.createThread(userId, personality);

    // Send initial message
    const response = await this.sendMessage(initialMessage, {
      threadId: thread.id,
      resourceId: userId,
      personality
    });

    return {
      threadId: thread.id,
      response,
      personality
    };
  }

  async continueConversation(threadId, message, personality) {
    return await this.sendMessage(message, {
      threadId,
      personality
    });
  }

  async createThread(userId, personality, title = null) {
    const threadData = {
      agentId: 'companionAgent',
      resourceId: userId,
      title: title || `Conversation ${new Date().toLocaleDateString()}`,
      metadata: {
        personality,
        createdAt: new Date().toISOString(),
        userId
      }
    };

    const response = await fetch('/api/memory/threads', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(threadData)
    });

    return await response.json();
  }

  async sendMessage(message, options) {
    const requestBody = {
      messages: [{ role: 'user', content: message }],
      resourceId: options.resourceId,
      ...options
    };

    const response = await fetch('/api/agents/companionAgent/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody)
    });

    return await response.json();
  }
}

Context Window Management

Monitor token usage to avoid hitting context limits and manage costs.
class TokenManager {
  constructor(maxTokens = 4000) {
    this.maxTokens = maxTokens;
    this.currentTokens = 0;
  }

  trackUsage(response) {
    this.currentTokens = response.totalUsage?.totalTokens || 0;
    return this.currentTokens;
  }

  shouldSummarize() {
    return this.currentTokens > this.maxTokens * 0.8;
  }

  async summarizeConversation(threadId, messages) {
    const summaryRequest = {
      messages: [
        {
          role: 'system',
          content: 'Summarize the key points of this conversation concisely.'
        },
        ...messages.slice(-10) // Last 10 messages
      ],
      threadId: `summary-${threadId}`,
      resourceId: 'system'
    };

    const response = await fetch('/api/agents/companionAgent/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(summaryRequest)
    });

    return await response.json();
  }
}

Performance Optimization

Streaming vs Non-Streaming

Use streaming for real-time UI updates and better user experience.
class StreamingChatManager {
  constructor() {
    this.activeStreams = new Map();
  }

  async sendStreamingMessage(message, options, callbacks) {
    const requestBody = {
      messages: [{ role: 'user', content: message }],
      ...options
    };

    const response = await fetch('/api/agents/companionAgent/stream', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) {
      throw new Error(`Stream request failed: ${response.status}`);
    }

    const streamId = Date.now().toString();
    this.activeStreams.set(streamId, response);

    await this.processStream(response, callbacks);
    this.activeStreams.delete(streamId);

    return streamId;
  }

  async processStream(response, callbacks) {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let fullText = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));
              await this.handleStreamEvent(data, callbacks);

              if (data.type === 'text') {
                fullText += data.text;
              }
            } catch (e) {
              console.warn('Failed to parse stream event:', e);
            }
          }
        }
      }

      callbacks?.onComplete?.(fullText);
    } catch (error) {
      callbacks?.onError?.(error);
    } finally {
      reader.releaseLock();
    }
  }

  async handleStreamEvent(event, callbacks) {
    switch (event.type) {
      case 'text':
        callbacks?.onText?.(event.text);
        break;
      case 'tool-call':
        callbacks?.onToolCall?.(event.payload);
        break;
      case 'tool-result':
        callbacks?.onToolResult?.(event.payload);
        break;
      case 'error':
        callbacks?.onError?.(new Error(event.error.message));
        break;
    }
  }

  cancelStream(streamId) {
    const response = this.activeStreams.get(streamId);
    if (response) {
      response.body.cancel();
      this.activeStreams.delete(streamId);
    }
  }
}

Caching Strategies

Cache tool results when appropriate to reduce API calls and improve performance.
class CacheManager {
  constructor(ttl = 300000) { // 5 minutes default
    this.cache = new Map();
    this.ttl = ttl;
  }

  set(key, value) {
    const expiry = Date.now() + this.ttl;
    this.cache.set(key, { value, expiry });
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }

  clear() {
    this.cache.clear();
  }

  cleanup() {
    const now = Date.now();
    for (const [key, item] of this.cache.entries()) {
      if (now > item.expiry) {
        this.cache.delete(key);
      }
    }
  }
}

class WeatherCache {
  constructor() {
    this.cache = new CacheManager(1800000); // 30 minutes for weather
  }

  getCachedWeather(location) {
    return this.cache.get(`weather_${location.toLowerCase()}`);
  }

  setCachedWeather(location, weatherData) {
    this.cache.set(`weather_${location.toLowerCase()}`, weatherData);
  }

  async getWeatherWithCache(location) {
    const cached = this.getCachedWeather(location);
    if (cached) {
      return { ...cached, cached: true };
    }

    // Fetch from API
    const response = await fetch('/api/agents/companionAgent/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        messages: [{ role: 'user', content: `What's the weather like in ${location}?` }],
        resourceId: 'system'
      })
    });

    const result = await response.json();

    // Extract weather data from tool results
    const weatherData = this.extractWeatherFromResponse(result);
    if (weatherData) {
      this.setCachedWeather(location, weatherData);
    }

    return { ...weatherData, cached: false };
  }

  extractWeatherFromResponse(response) {
    const toolResults = response.toolResults || [];
    const weatherResult = toolResults.find(r => r.toolName === 'weatherTool');
    return weatherResult?.result || null;
  }
}

Error Handling and Resilience

Comprehensive Error Recovery

Implement exponential backoff and circuit breaker patterns for reliability.
class ResilientAPIClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.retryHandler = new RetryHandler();
    this.circuitBreaker = new CircuitBreaker();
    this.errorTracker = new ErrorTracker();
  }

  async makeRequest(endpoint, data) {
    return await this.circuitBreaker.execute(async () => {
      return await this.retryHandler.executeWithRetry(async () => {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });

        if (!response.ok) {
          const errorData = await response.json();
          throw new APIError(errorData.error, response.status, errorData.code);
        }

        return await response.json();
      });
    });
  }

  async sendMessage(message, options = {}) {
    try {
      const requestData = {
        messages: [{ role: 'user', content: message }],
        ...options
      };

      return await this.makeRequest('/api/agents/companionAgent/generate', requestData);
    } catch (error) {
      this.errorTracker.trackError(error, {
        action: 'send_message',
        messageLength: message.length,
        personality: options.personality
      });

      // Return fallback response
      return await this.generateFallbackResponse(message, options);
    }
  }

  async generateFallbackResponse(message, options) {
    // Provide basic fallback when API is unavailable
    const fallbacks = {
      friend: "I'm here to support you. Could you tell me more about what's on your mind?",
      guru: "In times of uncertainty, remember that the eternal truth resides within.",
      wanderer: "Every journey has its challenges. What path are you walking today?"
    };

    return {
      text: fallbacks[options.personality] || fallbacks.friend,
      fallback: true,
      timestamp: new Date().toISOString()
    };
  }
}

Security Considerations

Input Validation

Always validate and sanitize user inputs to prevent injection attacks.
class InputValidator {
  static validateMessage(message) {
    if (!message || typeof message !== 'string') {
      throw new ValidationError('Message must be a non-empty string');
    }

    if (message.length > 10000) {
      throw new ValidationError('Message too long (max 10000 characters)');
    }

    // Check for potentially harmful content
    if (this.containsMaliciousContent(message)) {
      throw new ValidationError('Message contains inappropriate content');
    }

    return message.trim();
  }

  static validatePersonality(personality) {
    const validPersonalities = ['friend', 'guru', 'wanderer', 'philosopher'];
    if (!validPersonalities.includes(personality)) {
      throw new ValidationError(`Invalid personality: ${personality}`);
    }
    return personality;
  }

  static validateResourceId(resourceId) {
    if (!resourceId || typeof resourceId !== 'string') {
      throw new ValidationError('ResourceId must be a non-empty string');
    }

    // Basic sanitization
    const sanitized = resourceId.replace(/[^a-zA-Z0-9_-]/g, '');
    if (sanitized !== resourceId) {
      throw new ValidationError('ResourceId contains invalid characters');
    }

    return sanitized;
  }

  static containsMaliciousContent(text) {
    const maliciousPatterns = [
      /<script/i,
      /javascript:/i,
      /on\w+\s*=/i,
      /<iframe/i,
      /<object/i
    ];

    return maliciousPatterns.some(pattern => pattern.test(text));
  }
}

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

Rate Limiting

Implement client-side rate limiting to prevent abuse and manage costs.
class RateLimiter {
  constructor(requestsPerMinute = 60) {
    this.requestsPerMinute = requestsPerMinute;
    this.requests = [];
  }

  async acquire() {
    const now = Date.now();
    const oneMinuteAgo = now - 60000;

    // Remove old requests
    this.requests = this.requests.filter(time => time > oneMinuteAgo);

    if (this.requests.length >= this.requestsPerMinute) {
      const waitTime = 60000 - (now - this.requests[0]);
      await this.sleep(waitTime);
    }

    this.requests.push(now);
  }

  async sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const rateLimiter = new RateLimiter(30); // 30 requests per minute

async function sendRateLimitedMessage(message, options) {
  await rateLimiter.acquire();

  return await chat.sendMessage(message, options);
}

User Experience

Loading States and Feedback

Provide clear feedback during API calls and tool executions.
class UIManager {
  constructor() {
    this.loadingStates = new Map();
  }

  showLoading(elementId, message = 'Thinking...') {
    const element = document.getElementById(elementId);
    if (!element) return;

    const loadingId = Date.now().toString();
    this.loadingStates.set(loadingId, elementId);

    element.innerHTML = `
      <div class="loading-container">
        <div class="loading-spinner"></div>
        <div class="loading-text">${message}</div>
      </div>
    `;

    element.classList.add('loading');

    return loadingId;
  }

  updateLoading(loadingId, message) {
    const elementId = this.loadingStates.get(loadingId);
    if (!elementId) return;

    const element = document.getElementById(elementId);
    const textElement = element.querySelector('.loading-text');
    if (textElement) {
      textElement.textContent = message;
    }
  }

  hideLoading(loadingId) {
    const elementId = this.loadingStates.get(loadingId);
    if (!elementId) return;

    const element = document.getElementById(elementId);
    element.classList.remove('loading');
    element.innerHTML = '';

    this.loadingStates.delete(loadingId);
  }

  showToolExecution(toolName, toolCall) {
    const messages = {
      weatherTool: 'Checking the weather...',
      'search-destinations': 'Finding spiritual destinations...',
      'search-meditation-audio': 'Searching for meditation tracks...'
    };

    return this.showLoading('response-area', messages[toolName] || 'Working...');
  }

  showStreamingText(text) {
    const element = document.getElementById('response-area');
    if (!element) return;

    // Append text progressively for streaming effect
    element.innerHTML += text;
  }

  showError(message, retryable = false) {
    const element = document.getElementById('response-area');
    if (!element) return;

    element.innerHTML = `
      <div class="error-message">
        <div class="error-icon">⚠️</div>
        <div class="error-text">${message}</div>
        ${retryable ? '<button onclick="retryLastRequest()">Retry</button>' : ''}
      </div>
    `;
  }
}

Personality Selection Guidelines

Match personality to user needs and conversation context.
class PersonalityRecommender {
  static recommendPersonality(message, userHistory = []) {
    const lowerMessage = message.toLowerCase();

    // Deep spiritual questions → Guru
    if (this.containsKeywords(lowerMessage, ['enlightenment', 'truth', 'wisdom', 'ancient'])) {
      return 'guru';
    }

    // Travel and exploration → Wanderer
    if (this.containsKeywords(lowerMessage, ['travel', 'journey', 'visit', 'explore'])) {
      return 'wanderer';
    }

    // Philosophical inquiry → Philosopher
    if (this.containsKeywords(lowerMessage, ['why', 'meaning', 'existence', 'reality'])) {
      return 'philosopher';
    }

    // Emotional support → Friend
    if (this.containsKeywords(lowerMessage, ['help', 'feel', 'struggling', 'support'])) {
      return 'friend';
    }

    // Default based on user history
    const personalityUsage = this.analyzeHistory(userHistory);
    return personalityUsage.mostUsed || 'friend';
  }

  static containsKeywords(text, keywords) {
    return keywords.some(keyword => text.includes(keyword));
  }

  static analyzeHistory(history) {
    const personalityCount = {};

    history.forEach(item => {
      const personality = item.personality || 'friend';
      personalityCount[personality] = (personalityCount[personality] || 0) + 1;
    });

    const mostUsed = Object.keys(personalityCount).reduce((a, b) =>
      personalityCount[a] > personalityCount[b] ? a : b
    );

    return {
      mostUsed,
      totalInteractions: history.length,
      distribution: personalityCount
    };
  }
}

Production Deployment

Monitoring and Analytics

Implement comprehensive monitoring for performance and error tracking.
class APIMonitor {
  constructor() {
    this.metrics = {
      requests: 0,
      errors: 0,
      averageResponseTime: 0,
      tokenUsage: 0
    };

    this.responseTimes = [];
  }

  trackRequest(response, startTime) {
    this.metrics.requests++;
    const responseTime = Date.now() - startTime;
    this.responseTimes.push(responseTime);

    // Keep only last 100 response times
    if (this.responseTimes.length > 100) {
      this.responseTimes.shift();
    }

    this.metrics.averageResponseTime = this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length;

    if (response.totalUsage) {
      this.metrics.tokenUsage += response.totalUsage.totalTokens;
    }
  }

  trackError(error) {
    this.metrics.errors++;
    console.error('API Error tracked:', error);
  }

  getMetrics() {
    return {
      ...this.metrics,
      errorRate: this.metrics.requests > 0 ? (this.metrics.errors / this.metrics.requests) * 100 : 0
    };
  }

  reset() {
    this.metrics = {
      requests: 0,
      errors: 0,
      averageResponseTime: 0,
      tokenUsage: 0
    };
    this.responseTimes = [];
  }
}
These best practices will help you build robust, performant, and user-friendly applications with the Blue Oyster Personal Companion API.