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);
}
}
