Source: connectors/ClaudeConnector.js

/**
 * 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
            );

            logger.debug('Chat response generated successfully');
            
            // Extract the content string from the response
            // If the response is already a string, return it directly
            if (typeof response === 'string') {
                return response;
            }
            
            // If the response is an object with content, extract the content
            if (response && typeof response === 'object' && response.content) {
                return response.content;
            }
            
            // If the response has a different structure, try to convert to string
            return String(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
        }
    }
}