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.
Copy
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.
Copy
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.
Copy
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.
Copy
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.
Copy
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.
Copy
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.
Copy
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.
Copy
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.
Copy
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.
Copy
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 = [];
}
}
