Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.oyester.metaphy.live/llms.txt

Use this file to discover all available pages before exploring further.

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.