Skip to main content

Examples

This page provides complete, runnable examples for integrating with the Blue Oyster Personal Companion API.

Basic Chat Application

HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Blue Oyster Chat</title>
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 20px; }
    .chat-container { max-width: 800px; margin: 0 auto; }
    .messages { height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; margin-bottom: 10px; }
    .message { margin-bottom: 10px; padding: 8px; border-radius: 8px; }
    .user-message { background: #007bff; color: white; margin-left: 100px; }
    .assistant-message { background: #f8f9fa; margin-right: 100px; }
    .input-area { display: flex; gap: 10px; }
    input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
    button { padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; }
    .loading { opacity: 0.6; }
    .personality-selector { margin-bottom: 10px; }
    .personality-btn { padding: 5px 10px; margin: 0 5px; border: 1px solid #ddd; background: white; cursor: pointer; }
    .personality-btn.active { background: #007bff; color: white; }
  </style>
</head>
<body>
  <div class="chat-container">
    <h1>Blue Oyster Personal Companion</h1>

    <div class="personality-selector">
      <button class="personality-btn active" data-personality="friend">🤝 Friend</button>
      <button class="personality-btn" data-personality="guru">🧘 Guru</button>
      <button class="personality-btn" data-personality="wanderer">🗺️ Wanderer</button>
      <button class="personality-btn" data-personality="philosopher">🤔 Philosopher</button>
    </div>

    <div id="messages" class="messages"></div>

    <div class="input-area">
      <input type="text" id="messageInput" placeholder="Ask me anything about spirituality..." />
      <button onclick="sendMessage()">Send</button>
    </div>
  </div>

  <script src="chat.js"></script>
</body>
</html>

JavaScript Implementation

// chat.js
class BlueOysterChat {
  constructor() {
    this.baseUrl = 'https://oyester.metaphy.live';
    this.currentPersonality = 'friend';
    this.threadId = null;
    this.isLoading = false;

    this.initializeUI();
  }

  initializeUI() {
    // Personality selector
    document.querySelectorAll('.personality-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        this.setPersonality(btn.dataset.personality);
      });
    });

    // Message input
    document.getElementById('messageInput').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        this.sendMessage();
      }
    });
  }

  setPersonality(personality) {
    this.currentPersonality = personality;

    // Update UI
    document.querySelectorAll('.personality-btn').forEach(btn => {
      btn.classList.toggle('active', btn.dataset.personality === personality);
    });

    this.addMessage(`Switched to ${personality} personality`, 'system');
  }

  async sendMessage() {
    if (this.isLoading) return;

    const input = document.getElementById('messageInput');
    const message = input.value.trim();
    if (!message) return;

    input.value = '';
    this.addMessage(message, 'user');
    await this.processMessage(message);
  }

  async processMessage(message) {
    this.isLoading = true;
    this.showTypingIndicator();

    try {
      const response = await this.callAPI(message);
      this.hideTypingIndicator();
      this.addMessage(response.text, 'assistant');

      // Store thread ID for continuity
      if (response.threadId) {
        this.threadId = response.threadId;
      }
    } catch (error) {
      this.hideTypingIndicator();
      this.addMessage(`Error: ${error.message}`, 'error');
    } finally {
      this.isLoading = false;
    }
  }

  async callAPI(message) {
    const response = await fetch(`${this.baseUrl}/api/agents/companionAgent/generate`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        messages: [{ role: 'user', content: message }],
        runtimeContext: {
          metadata: {
            personality: this.currentPersonality
          }
        },
        threadId: this.threadId,
        resourceId: 'demo-user'
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error || `HTTP ${response.status}`);
    }

    return await response.json();
  }

  addMessage(text, type) {
    const messages = document.getElementById('messages');
    const messageDiv = document.createElement('div');
    messageDiv.className = `message ${type}-message`;

    if (type === 'system') {
      messageDiv.style.fontStyle = 'italic';
      messageDiv.style.color = '#666';
    } else if (type === 'error') {
      messageDiv.style.color = '#dc3545';
    }

    messageDiv.textContent = text;
    messages.appendChild(messageDiv);
    messages.scrollTop = messages.scrollHeight;
  }

  showTypingIndicator() {
    const messages = document.getElementById('messages');
    const indicator = document.createElement('div');
    indicator.className = 'message assistant-message typing-indicator';
    indicator.innerHTML = '<em>Thinking...</em>';
    indicator.id = 'typing-indicator';
    messages.appendChild(indicator);
    messages.scrollTop = messages.scrollHeight;
  }

  hideTypingIndicator() {
    const indicator = document.getElementById('typing-indicator');
    if (indicator) {
      indicator.remove();
    }
  }
}

// Initialize chat when page loads
const chat = new BlueOysterChat();

// Global function for button
function sendMessage() {
  chat.sendMessage();
}

Streaming Chat Application

Enhanced Streaming Implementation

class StreamingChat extends BlueOysterChat {
  constructor() {
    super();
    this.useStreaming = true;
  }

  async processMessage(message) {
    this.isLoading = true;
    this.showTypingIndicator();

    try {
      const fullResponse = await this.callStreamingAPI(message);
      this.hideTypingIndicator();
      // Response is built progressively, so just finalize
    } catch (error) {
      this.hideTypingIndicator();
      this.addMessage(`Error: ${error.message}`, 'error');
    } finally {
      this.isLoading = false;
    }
  }

  async callStreamingAPI(message) {
    const response = await fetch(`${this.baseUrl}/api/agents/companionAgent/stream`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        messages: [{ role: 'user', content: message }],
        runtimeContext: {
          metadata: {
            personality: this.currentPersonality
          }
        },
        threadId: this.threadId,
        resourceId: 'demo-user'
      })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error || `HTTP ${response.status}`);
    }

    return await this.processStream(response);
  }

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

    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, fullText, currentMessageDiv);
            } catch (e) {
              console.warn('Failed to parse stream event:', e);
            }
          }
        }
      }

      return fullText;
    } catch (error) {
      throw error;
    } finally {
      reader.releaseLock();
    }
  }

  async handleStreamEvent(event, fullText, currentMessageDiv) {
    switch (event.type) {
      case 'text':
        if (!currentMessageDiv) {
          this.hideTypingIndicator();
          currentMessageDiv = this.createStreamingMessage();
        }
        currentMessageDiv.textContent += event.text;
        document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
        break;

      case 'tool-call':
        this.addMessage(`🔧 Using ${event.payload.toolName}...`, 'system');
        break;

      case 'tool-result':
        const toolName = event.payload.toolName;
        const result = event.payload.result;

        if (toolName === 'weatherTool') {
          this.addMessage(`🌤️ Weather: ${result.temperature}°C, ${result.conditions}`, 'system');
        } else if (toolName === 'search-destinations') {
          this.addMessage(`📍 Found ${result.totalFound} spiritual destinations`, 'system');
        }
        break;

      case 'error':
        this.addMessage(`❌ Error: ${event.error.message}`, 'error');
        break;
    }
  }

  createStreamingMessage() {
    const messages = document.getElementById('messages');
    const messageDiv = document.createElement('div');
    messageDiv.className = 'message assistant-message';
    messages.appendChild(messageDiv);
    return messageDiv;
  }
}

// Use streaming version
const chat = new StreamingChat();

Meditation Timer with Audio

Complete Meditation App

// meditation-app.js
class MeditationApp {
  constructor() {
    this.chat = new BlueOysterChat();
    this.audioPlayer = new MeditationAudioPlayer();
    this.timer = new MeditationTimer();
    this.currentSession = null;

    this.initializeUI();
  }

  initializeUI() {
    // Timer controls
    document.getElementById('start-timer').addEventListener('click', () => {
      this.startMeditationSession();
    });

    document.getElementById('stop-timer').addEventListener('click', () => {
      this.stopMeditationSession();
    });

    // Duration selector
    document.getElementById('duration').addEventListener('change', (e) => {
      this.timer.setDuration(parseInt(e.target.value));
    });
  }

  async startMeditationSession() {
    const mood = document.getElementById('meditation-mood').value;
    const duration = parseInt(document.getElementById('duration').value);

    // Get guidance for the session
    const guidance = await this.getMeditationGuidance(mood, duration);

    // Find and play appropriate audio
    await this.loadMeditationAudio(mood);

    // Start the timer
    this.timer.start(duration * 60, (remaining) => {
      this.updateTimerDisplay(remaining);
    }, () => {
      this.onSessionComplete();
    });

    this.currentSession = { mood, duration, guidance };
    this.updateUI('active');
  }

  async getMeditationGuidance(mood, duration) {
    const prompt = `Guide me through a ${duration}-minute ${mood} meditation session. Provide step-by-step instructions.`;

    const response = await this.chat.callAPI(prompt);
    return response.text;
  }

  async loadMeditationAudio(mood) {
    // Search for appropriate meditation audio
    const searchResponse = await this.chat.callAPI(
      `Find ${mood} meditation audio tracks for a ${duration}-minute session`
    );

    // Extract audio URLs from tool results
    const audioResults = searchResponse.toolResults?.find(
      r => r.toolName === 'search-meditation-audio'
    );

    if (audioResults?.result?.tracks?.length > 0) {
      const track = audioResults.result.tracks[0];
      this.audioPlayer.playTrack(track);
    }
  }

  stopMeditationSession() {
    this.timer.stop();
    this.audioPlayer.pause();
    this.updateUI('stopped');
  }

  onSessionComplete() {
    this.audioPlayer.fadeOut();
    this.updateUI('completed');

    // Get completion message
    setTimeout(async () => {
      const completionMessage = await this.getCompletionMessage();
      this.chat.addMessage(completionMessage, 'assistant');
    }, 2000);
  }

  async getCompletionMessage() {
    const response = await this.chat.callAPI(
      'I just completed my meditation session. What are your thoughts?'
    );
    return response.text;
  }

  updateTimerDisplay(seconds) {
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    document.getElementById('timer-display').textContent =
      `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
  }

  updateUI(state) {
    const startBtn = document.getElementById('start-timer');
    const stopBtn = document.getElementById('stop-timer');

    switch (state) {
      case 'active':
        startBtn.disabled = true;
        stopBtn.disabled = false;
        break;
      case 'stopped':
      case 'completed':
        startBtn.disabled = false;
        stopBtn.disabled = true;
        break;
    }
  }
}

class MeditationTimer {
  constructor() {
    this.interval = null;
    this.duration = 5; // minutes
    this.remaining = 0;
  }

  setDuration(minutes) {
    this.duration = minutes;
  }

  start(durationSeconds, onTick, onComplete) {
    this.remaining = durationSeconds;
    this.interval = setInterval(() => {
      this.remaining--;

      if (this.remaining <= 0) {
        this.stop();
        onComplete();
      } else {
        onTick(this.remaining);
      }
    }, 1000);
  }

  stop() {
    if (this.interval) {
      clearInterval(this.interval);
      this.interval = null;
    }
  }
}

class MeditationAudioPlayer {
  constructor() {
    this.audio = new Audio();
    this.currentTrack = null;
  }

  async playTrack(track) {
    this.currentTrack = track;
    this.audio.src = track.audioUrl;
    this.audio.volume = 0.3; // Gentle volume

    try {
      await this.audio.play();
    } catch (error) {
      console.warn('Audio playback failed:', error);
    }
  }

  pause() {
    this.audio.pause();
  }

  fadeOut() {
    const fadeStep = 0.05;
    const fadeInterval = setInterval(() => {
      if (this.audio.volume > fadeStep) {
        this.audio.volume -= fadeStep;
      } else {
        this.audio.volume = 0;
        this.audio.pause();
        clearInterval(fadeInterval);
      }
    }, 100);
  }
}

// HTML for meditation app
const meditationHTML = `
  <div class="meditation-app">
    <h2>Meditation Session</h2>

    <div class="session-controls">
      <select id="meditation-mood">
        <option value="calm">Calm</option>
        <option value="peaceful">Peaceful</option>
        <option value="focused">Focused</option>
        <option value="relaxing">Relaxing</option>
      </select>

      <select id="duration">
        <option value="5">5 minutes</option>
        <option value="10">10 minutes</option>
        <option value="15">15 minutes</option>
        <option value="20">20 minutes</option>
      </select>

      <button id="start-timer">Start Session</button>
      <button id="stop-timer" disabled>Stop</button>
    </div>

    <div id="timer-display" class="timer">5:00</div>

    <div id="guidance" class="guidance-area"></div>
  </div>
`;

// Initialize the app
const meditationApp = new MeditationApp();

Weather-Aware Spiritual Planning

Travel Planning Integration

class SpiritualTravelPlanner {
  constructor(chat) {
    this.chat = chat;
    this.weatherCache = new Map();
  }

  async planSpiritualTrip(destination, interests) {
    // Get weather for the destination
    const weather = await this.getWeather(destination);

    // Find spiritual destinations
    const destinations = await this.findDestinations(destination, interests);

    // Create personalized itinerary
    const itinerary = await this.createItinerary(destinations, weather, interests);

    return {
      destination,
      weather,
      destinations,
      itinerary,
      recommendations: this.generateRecommendations(weather, destinations)
    };
  }

  async getWeather(location) {
    if (this.weatherCache.has(location)) {
      return this.weatherCache.get(location);
    }

    const response = await this.chat.callAPI(`What's the weather like in ${location}?`);
    const weatherData = this.extractWeatherFromResponse(response);

    if (weatherData) {
      this.weatherCache.set(location, weatherData);
    }

    return weatherData;
  }

  async findDestinations(location, interests) {
    const query = `Find spiritual destinations in ${location} related to ${interests.join(', ')}`;

    const response = await this.chat.callAPI(query);
    const toolResults = response.toolResults || [];
    const destinationResult = toolResults.find(r => r.toolName === 'search-destinations');

    return destinationResult?.result?.destinations || [];
  }

  async createItinerary(destinations, weather, interests) {
    const destinationNames = destinations.slice(0, 3).map(d => d.name).join(', ');
    const query = `Create a spiritual itinerary for ${destinationNames} considering ${weather.conditions} weather. Focus on ${interests.join(', ')} activities.`;

    const response = await this.chat.callAPI(query);
    return response.text;
  }

  generateRecommendations(weather, destinations) {
    const recommendations = [];

    if (weather.temperature < 15) {
      recommendations.push('Bring warm clothing for outdoor activities');
    }

    if (weather.humidity > 70) {
      recommendations.push('Stay hydrated, especially during meditation');
    }

    if (destinations.some(d => d.category === 'temple')) {
      recommendations.push('Visit temples early morning for peaceful atmosphere');
    }

    return recommendations;
  }

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

// Usage
const travelPlanner = new SpiritualTravelPlanner(chat);

async function planTrip() {
  const trip = await travelPlanner.planSpiritualTrip('Kyoto, Japan', [
    'Zen meditation',
    'temple visits',
    'traditional tea ceremony'
  ]);

  console.log('Spiritual Trip Plan:', trip);
}

Voice Integration with Speech-to-Text

Voice-Enabled Chat

class VoiceEnabledChat extends StreamingChat {
  constructor() {
    super();
    this.recognition = null;
    this.synthesis = new SpeechSynthesisUtterance();
    this.isListening = false;

    this.initializeVoice();
  }

  initializeVoice() {
    // Check for Web Speech API support
    if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
      const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
      this.recognition = new SpeechRecognition();
      this.recognition.continuous = false;
      this.recognition.interimResults = false;
      this.recognition.lang = 'en-US';

      this.recognition.onresult = (event) => {
        const transcript = event.results[0][0].transcript;
        document.getElementById('messageInput').value = transcript;
        this.sendMessage();
      };

      this.recognition.onerror = (event) => {
        console.error('Speech recognition error:', event.error);
        this.stopListening();
      };

      this.recognition.onend = () => {
        this.stopListening();
      };
    }

    // Voice synthesis
    this.synthesis.rate = 0.9;
    this.synthesis.pitch = 1;
  }

  startListening() {
    if (!this.recognition) {
      alert('Speech recognition not supported in this browser');
      return;
    }

    if (this.isListening) return;

    this.isListening = true;
    this.recognition.start();

    // Update UI
    document.getElementById('voice-btn').classList.add('listening');
    document.getElementById('voice-btn').textContent = '🎤 Listening...';
  }

  stopListening() {
    if (this.recognition && this.isListening) {
      this.recognition.stop();
    }

    this.isListening = false;

    // Update UI
    document.getElementById('voice-btn').classList.remove('listening');
    document.getElementById('voice-btn').textContent = '🎤';
  }

  speakText(text) {
    if ('speechSynthesis' in window) {
      this.synthesis.text = text;
      window.speechSynthesis.speak(this.synthesis);
    }
  }

  async processVoiceMessage(audioBlob) {
    try {
      // Convert blob to base64
      const base64Audio = await this.blobToBase64(audioBlob);

      // Send to speech-to-text endpoint
      const transcript = await this.transcribeAudio(base64Audio);

      // Process the transcribed text
      document.getElementById('messageInput').value = transcript;
      await this.sendMessage();

    } catch (error) {
      console.error('Voice processing error:', error);
      this.addMessage('Sorry, I couldn\'t understand that audio. Please try again.', 'error');
    }
  }

  async transcribeAudio(base64Audio) {
    const formData = new FormData();
    // Convert base64 back to blob for upload
    const audioBlob = this.base64ToBlob(base64Audio);
    formData.append('audio', audioBlob);

    const response = await fetch(`${this.baseUrl}/stt`, {
      method: 'POST',
      body: formData
    });

    if (!response.ok) {
      throw new Error('Speech-to-text failed');
    }

    const result = await response.json();
    return result.transcript;
  }

  blobToBase64(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result.split(',')[1]);
      reader.onerror = reject;
      reader.readAsDataURL(blob);
    });
  }

  base64ToBlob(base64) {
    const byteCharacters = atob(base64);
    const byteNumbers = new Array(byteCharacters.length);
    for (let i = 0; i < byteCharacters.length; i++) {
      byteNumbers[i] = byteCharacters.charCodeAt(i);
    }
    const byteArray = new Uint8Array(byteNumbers);
    return new Blob([byteArray], { type: 'audio/wav' });
  }

  async handleStreamEvent(event, fullText, currentMessageDiv) {
    // Call parent method
    await super.handleStreamEvent(event, fullText, currentMessageDiv);

    // Auto-speak completed responses
    if (event.type === 'run-finish' && fullText) {
      setTimeout(() => this.speakText(fullText), 500);
    }
  }
}

// Enhanced HTML with voice controls
const voiceChatHTML = `
  <div class="voice-controls">
    <button id="voice-btn" onclick="toggleVoice()">🎤</button>
    <button onclick="speakLastResponse()">🔊</button>
  </div>
`;

// Global functions
function toggleVoice() {
  if (chat.isListening) {
    chat.stopListening();
  } else {
    chat.startListening();
  }
}

function speakLastResponse() {
  const messages = document.querySelectorAll('.assistant-message');
  const lastMessage = messages[messages.length - 1];
  if (lastMessage) {
    chat.speakText(lastMessage.textContent);
  }
}
These examples provide complete, runnable code for various integration scenarios with the Blue Oyster Personal Companion API.