UNPKG

@bear_ai/nexus-flow

Version:

Universal AI orchestrator - portal to any flow (claude-flow, qwen-flow, copilot-flow) with queen bee coordination system

540 lines (454 loc) 18.4 kB
import { BaseFlowAdapter, FlowAdapterConfig, FlowCapabilities, FlowExecutionContext } from './base-flow-adapter.js'; import { Task, TaskResult, FlowType, FlowStatus } from '../types/index.js'; import { Logger } from '../utils/logger.js'; import axios, { AxiosInstance, AxiosError } from 'axios'; export interface PerplexityFlowConfig extends FlowAdapterConfig { apiKey?: string; endpoint?: string; model?: string; maxTokens?: number; temperature?: number; topP?: number; topK?: number; presencePenalty?: number; frequencyPenalty?: number; webSearchEnabled?: boolean; citationsEnabled?: boolean; streamEnabled?: boolean; webAuthConfig?: { homeUrl: string; settingsUrl: string; apiKeysUrl: string; }; } export interface PerplexityMessage { role: 'system' | 'user' | 'assistant'; content: string; } export interface PerplexitySearchResult { title: string; url: string; content: string; publishedDate?: string; } export interface PerplexityResponse { id: string; object: string; created: number; model: string; choices: Array<{ index: number; message: PerplexityMessage; finish_reason: string; }>; usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; citations?: string[]; search_results?: PerplexitySearchResult[]; } export class PerplexityFlowAdapter extends BaseFlowAdapter { private logger: Logger; private httpClient: AxiosInstance; private config: PerplexityFlowConfig; // Available Perplexity models private static readonly AVAILABLE_MODELS = { 'llama-3.1-sonar-small-128k-online': 'Sonar Small Online - Fast responses with web search', 'llama-3.1-sonar-large-128k-online': 'Sonar Large Online - High-quality responses with web search', 'llama-3.1-sonar-huge-128k-online': 'Sonar Huge Online - Best quality responses with web search', 'llama-3.1-8b-instruct': 'Llama 3.1 8B - Fast offline model', 'llama-3.1-70b-instruct': 'Llama 3.1 70B - High-quality offline model', 'codellama-34b-instruct': 'Code Llama 34B - Specialized for coding tasks' }; constructor(config: PerplexityFlowConfig) { super(config); this.config = config; this.logger = new Logger('PerplexityFlowAdapter'); // Set default values this.config.endpoint = this.config.endpoint || 'https://api.perplexity.ai'; this.config.model = this.config.model || 'llama-3.1-sonar-large-128k-online'; this.config.maxTokens = this.config.maxTokens || 4096; this.config.temperature = this.config.temperature || 0.2; this.config.topP = this.config.topP || 0.9; this.config.topK = this.config.topK || 0; this.config.presencePenalty = this.config.presencePenalty || 0; this.config.frequencyPenalty = this.config.frequencyPenalty || 1; this.config.webSearchEnabled = this.config.webSearchEnabled !== false; this.config.citationsEnabled = this.config.citationsEnabled !== false; this.config.streamEnabled = this.config.streamEnabled || false; // Default web auth configuration this.config.webAuthConfig = this.config.webAuthConfig || { homeUrl: 'https://www.perplexity.ai', settingsUrl: 'https://www.perplexity.ai/settings', apiKeysUrl: 'https://www.perplexity.ai/settings/api' }; this.httpClient = axios.create({ baseURL: this.config.endpoint, timeout: this.config.timeout || 45000, // Longer timeout for research tasks headers: { 'Content-Type': 'application/json', 'User-Agent': 'nexus-flow-perplexity-adapter/1.0.0' } }); this.setupHttpInterceptors(); this.validateConfig(); } get name(): string { return 'perplexity-flow'; } get type(): FlowType { return FlowType.PERPLEXITY; } get version(): string { return '1.0.0'; } async initialize(): Promise<void> { this.logger.info('Initializing Perplexity Flow adapter...'); try { // Check authentication if (!this.isAuthenticated()) { throw new Error('Perplexity API key not configured. Please authenticate first.'); } // Test API connectivity const isHealthy = await this.checkHealth(); if (!isHealthy) { throw new Error('Perplexity API health check failed'); } this.setStatus(FlowStatus.AVAILABLE); this.logger.info(`Perplexity Flow adapter initialized successfully with model: ${this.config.model}`); } catch (error: any) { this.logger.error('Failed to initialize Perplexity Flow adapter:', error); this.setStatus(FlowStatus.ERROR); throw error; } } async shutdown(): Promise<void> { this.logger.info('Shutting down Perplexity Flow adapter...'); this.setStatus(FlowStatus.OFFLINE); this.logger.info('Perplexity Flow adapter shutdown complete'); } async executeTask(task: Task, context?: FlowExecutionContext): Promise<TaskResult> { if (!this.canAcceptTask()) { throw new Error(`Perplexity Flow adapter cannot accept task: status=${this.status}, load=${this.currentLoad}/${this.getMaxLoad()}`); } this.incrementLoad(); const startTime = Date.now(); try { const systemPrompt = this.buildSystemPrompt(task, context); const userPrompt = this.buildUserPrompt(task); const messages: PerplexityMessage[] = [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ]; // Select appropriate model based on task type const model = this.selectModelForTask(task); const response = await this.executeCompletion(messages, model); const executionTime = Date.now() - startTime; this.logger.info(`Task completed: ${task.id} in ${executionTime}ms`); return this.createTaskResult( true, this.formatResponse(response), undefined, executionTime, { model: response.model, usage: response.usage, citations: response.citations?.length || 0, searchResults: response.search_results?.length || 0, hasWebSearch: this.isOnlineModel(model) } ); } catch (error: any) { const executionTime = Date.now() - startTime; this.logger.error(`Task failed: ${task.id} - ${error.message}`); return this.createTaskResult( false, undefined, this.handlePerplexityError(error), executionTime, { errorType: error.constructor.name, statusCode: error.response?.status } ); } finally { this.decrementLoad(); } } async checkHealth(): Promise<boolean> { try { // Test with a simple completion const response = await this.httpClient.post('/chat/completions', { model: 'llama-3.1-8b-instruct', // Use fastest model for health check messages: [{ role: 'user', content: 'Hello' }], max_tokens: 10, temperature: 0 }, { headers: { 'Authorization': `Bearer ${this.config.apiKey || process.env.PERPLEXITY_API_KEY}` } }); return response.status === 200 && response.data.choices?.length > 0; } catch (error: any) { this.logger.error('Perplexity health check failed:', error.message); return false; } } getCapabilities(): FlowCapabilities { return { codeGeneration: true, codeReview: true, research: true, // Perplexity's strongest capability analysis: true, documentation: true, testing: false, // Limited testing capabilities refactoring: true, orchestration: false, // No native orchestration support hiveMind: false, // No hive-mind capabilities swarmCoordination: false, // Limited coordination features mcp: false, // No native MCP support webAuth: true // Supports web authentication }; } async authenticate(): Promise<boolean> { // For Perplexity, web authentication involves getting API key from settings if (this.config.authConfig?.type === 'web') { const authUrl = this.getAuthUrl(); if (authUrl) { this.logger.info('Opening Perplexity authentication URL...'); await this.openAuthUrl(authUrl); this.logger.info('Please create an API key in your Perplexity settings and set it in your environment.'); this.logger.info('Set PERPLEXITY_API_KEY environment variable or update your nexus-flow configuration.'); return false; // User needs to manually configure API key } } return this.isAuthenticated(); } isAuthenticated(): boolean { return !!(this.config.apiKey || process.env.PERPLEXITY_API_KEY); } getAuthUrl(): string { return this.config.webAuthConfig?.apiKeysUrl || 'https://www.perplexity.ai/settings/api'; } private buildSystemPrompt(task: Task, context?: FlowExecutionContext): string { let prompt = `You are Perplexity AI, an advanced AI assistant with powerful research and analysis capabilities, integrated into the Nexus Flow orchestration system. Your key strengths include: - Real-time web search and current information access - Comprehensive research with citations - Factual accuracy and source verification - In-depth analysis and reasoning Current task type: ${task.type} Priority: ${task.priority} `; if (this.config.webSearchEnabled && this.isOnlineModel(this.config.model!)) { prompt += '\nYou have access to real-time web search capabilities. Use this to provide current, accurate information.'; } if (this.config.citationsEnabled) { prompt += '\nAlways provide citations and sources for your information when available.'; } if (context?.workingDirectory) { prompt += `\nWorking directory: ${context.workingDirectory}`; } if (context?.environmentVars) { prompt += `\nEnvironment variables: ${JSON.stringify(context.environmentVars, null, 2)}`; } // Task-specific guidance switch (task.type) { case 'research': prompt += '\nFor research tasks, prioritize accuracy, comprehensiveness, and current information.'; break; case 'code-generation': prompt += '\nFor coding tasks, provide working code with explanations and best practices.'; break; case 'analysis': prompt += '\nFor analysis tasks, break down complex problems systematically and provide detailed insights.'; break; case 'documentation': prompt += '\nFor documentation tasks, create clear, comprehensive, and well-structured documentation.'; break; } return prompt; } private buildUserPrompt(task: Task): string { let prompt = `Task: ${task.description}`; if (task.metadata) { prompt += `\n\nAdditional context: ${JSON.stringify(task.metadata, null, 2)}`; } return prompt; } private selectModelForTask(task: Task): string { // Select optimal model based on task type and requirements switch (task.type) { case 'research': return 'llama-3.1-sonar-large-128k-online'; // Best for research with web search case 'code-generation': case 'code-review': case 'refactoring': return 'codellama-34b-instruct'; // Specialized for coding case 'analysis': return this.config.webSearchEnabled ? 'llama-3.1-sonar-huge-128k-online' : 'llama-3.1-70b-instruct'; default: return this.config.model || 'llama-3.1-sonar-large-128k-online'; } } private isOnlineModel(model: string): boolean { return model.includes('sonar') && model.includes('online'); } private async executeCompletion(messages: PerplexityMessage[], model: string): Promise<PerplexityResponse> { const requestPayload: any = { model, messages, max_tokens: this.config.maxTokens, temperature: this.config.temperature, top_p: this.config.topP, presence_penalty: this.config.presencePenalty, frequency_penalty: this.config.frequencyPenalty, stream: this.config.streamEnabled }; // Add top_k only if it's greater than 0 (Perplexity specific) if (this.config.topK && this.config.topK > 0) { requestPayload.top_k = this.config.topK; } // Enable citations for online models if (this.isOnlineModel(model) && this.config.citationsEnabled) { requestPayload.return_citations = true; requestPayload.return_images = false; // Focus on text content } const response = await this.httpClient.post('/chat/completions', requestPayload, { headers: { 'Authorization': `Bearer ${this.config.apiKey || process.env.PERPLEXITY_API_KEY}` } }); return response.data; } private formatResponse(response: PerplexityResponse): string { let formattedResponse = response.choices[0].message.content; // Add citations if available if (response.citations && response.citations.length > 0 && this.config.citationsEnabled) { formattedResponse += '\n\n**Sources:**\n'; response.citations.forEach((citation, index) => { formattedResponse += `[${index + 1}] ${citation}\n`; }); } // Add search results summary if available if (response.search_results && response.search_results.length > 0) { formattedResponse += '\n\n**Research Sources:**\n'; response.search_results.slice(0, 3).forEach((result, index) => { formattedResponse += `${index + 1}. **${result.title}**\n ${result.url}\n`; if (result.publishedDate) { formattedResponse += ` Published: ${result.publishedDate}\n`; } formattedResponse += '\n'; }); } return formattedResponse; } private handlePerplexityError(error: AxiosError): string { if (error.response) { const status = error.response.status; const data = error.response.data as any; switch (status) { case 401: return 'Authentication failed. Please check your Perplexity API key.'; case 429: return 'Rate limit exceeded. Perplexity has usage limits. Please try again later.'; case 400: return `Bad request: ${data?.error?.message || data?.message || 'Invalid parameters'}`; case 402: return 'Payment required. Please check your Perplexity account billing status.'; case 500: return 'Perplexity API server error. Please try again later.'; case 503: return 'Perplexity service temporarily unavailable. Please try again later.'; default: return `API error (${status}): ${data?.error?.message || data?.message || error.message}`; } } if (error.code === 'ECONNREFUSED') { return 'Cannot connect to Perplexity API. Please check your internet connection.'; } if (error.code === 'ETIMEDOUT') { return 'Request timed out. Perplexity may be experiencing high load.'; } return error.message || 'Unknown error occurred'; } private setupHttpInterceptors(): void { // Request interceptor for logging and rate limiting this.httpClient.interceptors.request.use( (config) => { this.logger.debug(`Making request to Perplexity: ${config.method?.toUpperCase()} ${config.url}`); return config; }, (error) => { this.logger.error('Request interceptor error:', error); return Promise.reject(error); } ); // Response interceptor for error handling and logging this.httpClient.interceptors.response.use( (response) => { this.logger.debug(`Perplexity response: ${response.status} ${response.statusText}`); // Log usage information if (response.data.usage) { this.logger.debug(`Token usage: ${response.data.usage.total_tokens} total, ${response.data.usage.completion_tokens} completion`); } return response; }, (error) => { this.logger.error('Perplexity API error:', error.response?.status, error.message); // Log rate limiting information if (error.response?.status === 429) { const resetTime = error.response.headers['x-ratelimit-reset']; if (resetTime) { this.logger.warn(`Rate limit reset time: ${new Date(resetTime * 1000).toISOString()}`); } } return Promise.reject(error); } ); } protected validateConfig(): void { super.validateConfig(); const config = this.config as PerplexityFlowConfig; if (!config.endpoint) { throw new Error('Perplexity API endpoint is required'); } if (!config.model) { throw new Error('Perplexity model is required'); } // Validate model selection if (!Object.keys(PerplexityFlowAdapter.AVAILABLE_MODELS).includes(config.model)) { this.logger.warn(`Unknown model: ${config.model}. Available models:`, Object.keys(PerplexityFlowAdapter.AVAILABLE_MODELS)); } if (config.temperature !== undefined && (config.temperature < 0 || config.temperature > 2)) { throw new Error('Temperature must be between 0 and 2'); } if (config.topP !== undefined && (config.topP < 0 || config.topP > 1)) { throw new Error('topP must be between 0 and 1'); } if (config.maxTokens !== undefined && config.maxTokens <= 0) { throw new Error('maxTokens must be greater than 0'); } if (config.topK !== undefined && config.topK < 0) { throw new Error('topK must be 0 or greater'); } if (config.presencePenalty !== undefined && (config.presencePenalty < -2 || config.presencePenalty > 2)) { throw new Error('presencePenalty must be between -2 and 2'); } if (config.frequencyPenalty !== undefined && (config.frequencyPenalty < -2 || config.frequencyPenalty > 2)) { throw new Error('frequencyPenalty must be between -2 and 2'); } } // Helper method to get available models static getAvailableModels(): Record<string, string> { return PerplexityFlowAdapter.AVAILABLE_MODELS; } // Helper method to check if web search is available for current model hasWebSearchCapability(): boolean { return this.config.webSearchEnabled && this.isOnlineModel(this.config.model!); } }