UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

289 lines (256 loc) 10.1 kB
/** * Connector for Anthropic Claude API operations using hyperdata-clients */ import logger from 'loglevel'; import { ClientFactory } from 'hyperdata-clients'; export default class ClaudeConnector { /** * Create a new HClaudeClientConnector * @param {string} apiKey - Claude API key * @param {string} defaultModel - Optional default model to use */ constructor(apiKey, defaultModel = 'claude-3-opus-20240229') { if (!apiKey) { throw new Error('Claude API key is required'); } this.apiKey = apiKey; this.defaultModel = defaultModel; this.client = null; this.initialized = false; this.initializing = false; // Initialize the client this.initialize(); } /** * Initialize the Claude client * @returns {Promise<void>} */ async initialize() { if (this.initialized || this.initializing) { return; } this.initializing = true; try { this.client = await ClientFactory.createAPIClient('claude', { apiKey: this.apiKey, model: this.defaultModel, clientOptions: { // Add any specific client options here } }); this.initialized = true; logger.info('Claude client initialized successfully'); } catch (error) { logger.error('Failed to initialize Claude client:', error); this.initialized = false; this.initializing = false; throw error; } finally { this.initializing = false; } } /** * Initialize the Claude client */ async initialize() { if (this.initialized) return; if (this.initializing) { // If already initializing, wait for it to complete return new Promise((resolve) => { const checkInitialized = () => { if (this.initialized) resolve(); else setTimeout(checkInitialized, 100); }; checkInitialized(); }); } this.initializing = true; try { this.client = await ClientFactory.createAPIClient('claude', { apiKey: this.apiKey, model: this.defaultModel, clientOptions: { // Add any specific client options here } }); this.initialized = true; logger.debug('Claude client initialized successfully'); } catch (error) { logger.error('Failed to initialize Claude client:', error); this.initialized = false; this.initializing = false; throw error; } finally { this.initializing = false; } } /** * Generate embeddings using Claude * @param {string} model - Model name to use for embedding * @param {string} input - Text to generate embedding for * @returns {number[]} - Vector embedding */ async generateEmbedding(model, input) { logger.debug(`Generating embedding with model ${model}`); logger.debug('Input length:', input.length); try { await this.initialize(); // Convert single string to array if needed const inputs = Array.isArray(input) ? input : [input]; // Generate embeddings for all inputs const embeddings = []; for (const text of inputs) { try { // Try to use Claude's embedding endpoint if available const response = await this.client.embedding(text, { model: model || this.defaultModel }); embeddings.push(response); } catch (error) { logger.warn('Claude embedding failed, falling back to local embedding model'); // Fallback to a simple local embedding if Claude's embedding fails const embedding = this._generateSimpleEmbedding(text); embeddings.push(embedding); } } logger.debug('Embedding generation successful'); return embeddings.length === 1 ? embeddings[0] : embeddings; } catch (error) { logger.error('Embedding generation failed:', error); throw error; } } /** * Generate a simple local embedding as a fallback * @private */ _generateSimpleEmbedding(text) { // This is a simple hash-based embedding generator as a fallback // It's not as good as a real embedding model but better than nothing const hash = this._hashCode(text); const embedding = new Array(1536).fill(0); // Distribute the hash values across the embedding dimensions for (let i = 0; i < 1536; i++) { const val = (hash * (i + 1)) % 1.0; // Simple pseudo-random value based on hash and position embedding[i] = (val - 0.5) * 2; // Scale to [-1, 1] } return embedding; } /** * Simple string hash function * @private */ _hashCode(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash); } /** * Generate chat completion using Claude * @param {string} model - Model name to use * @param {Array} messages - Array of message objects with role and content * @param {Object} options - Additional options * @returns {string} - Response text */ async generateChat(model, messages, options = {}) { logger.debug(`Generating chat with model ${model}`); logger.debug('Messages:', messages); try { await this.initialize(); // For Claude API, we need to handle system messages differently // System message should be a top-level parameter, not in the messages array let systemMessage = ''; const filteredMessages = []; // Separate system messages from user/assistant messages for (const msg of messages) { if (msg.role === 'system') { systemMessage += (systemMessage ? '\n' : '') + msg.content; } else { filteredMessages.push({ role: msg.role, content: msg.content }); } } // Prepare the request options with Claude API expected parameter names const requestOptions = { model: model || this.defaultModel, max_tokens: options.maxTokens || 1024, // Note: max_tokens instead of maxTokens temperature: options.temperature ?? 0.7, top_p: options.topP, // Note: top_p instead of topP ...options }; // Remove any undefined options Object.keys(requestOptions).forEach(key => { if (requestOptions[key] === undefined) { delete requestOptions[key]; } }); // Add system message as a top-level parameter if present if (systemMessage) { requestOptions.system = systemMessage; } // The first argument should be the messages array const response = await this.client.chat( filteredMessages, requestOptions ); // If the response is a string, wrap it in a proper response object if (typeof response === 'string') { return { content: response, role: 'assistant' }; } logger.debug('Chat response generated successfully'); return response; } catch (error) { logger.error('Chat generation failed:', error); throw error; } } /** * Generate completion using Claude * @param {string} model - Model name to use * @param {string} prompt - Text prompt * @param {Object} options - Additional options * @returns {string} - Response text */ async generateCompletion(model, prompt, options = {}) { logger.debug(`Generating completion with model ${model}`) logger.debug('Prompt length:', prompt.length) try { if (!this.client) { await this.initialize() } // Convert to chat format since Claude uses chat API for completions const response = await this.client.complete(prompt, { model, temperature: options.temperature || 0.7, max_tokens: options.max_tokens || 1024, ...options }) logger.debug('Completion response received') // Extract content string from the response object // Based on the Claude response format: {content: '...', role: 'assistant'} if (response && typeof response === 'object' && response.content) { logger.debug('Extracting content from Claude response object') return response.content; } else if (typeof response === 'string') { logger.debug('Response is already a string') return response; } else { logger.error('Unexpected response format from Claude completion:', response); // Fallback - return empty string to avoid JSON parse errors return ''; } } catch (error) { logger.error('Completion generation failed:', error) throw error } } }