import logging import httpx from typing import List from fastapi import HTTPException from modules.shared.configuration import APP_CONFIG from modules.aicore.aicoreBase import BaseConnectorAi from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings # Configure logger logger = logging.getLogger(__name__) def loadConfigData(): """Load configuration data for Perplexity connector""" return { "apiKey": APP_CONFIG.get('Connector_AiPerplexity_API_SECRET'), } class AiPerplexity(BaseConnectorAi): """Connector for communication with the Perplexity API.""" def __init__(self): super().__init__() # Load configuration self.config = loadConfigData() self.apiKey = self.config["apiKey"] # HttpClient for API calls self.httpClient = httpx.AsyncClient( timeout=120.0, # Longer timeout for complex requests headers={ "Authorization": f"Bearer {self.apiKey}", "Content-Type": "application/json", "Accept": "application/json" } ) logger.info("Perplexity Connector initialized") def getConnectorType(self) -> str: """Get the connector type identifier.""" return "perplexity" def getModels(self) -> List[AiModel]: """Get all available Perplexity models.""" return [ AiModel( name="sonar", displayName="Perplexity Sonar", connectorType="perplexity", apiUrl="https://api.perplexity.ai/chat/completions", temperature=0.2, maxTokens=4000, contextLength=32000, costPer1kTokensInput=0.005, costPer1kTokensOutput=0.005, speedRating=8, qualityRating=8, # capabilities removed (not used in business logic) functionCall=self.callWebOperation, priority=PriorityEnum.BALANCED, processingMode=ProcessingModeEnum.ADVANCED, operationTypes=createOperationTypeRatings( (OperationTypeEnum.WEB_RESEARCH, 8), (OperationTypeEnum.WEB_SEARCH, 9), (OperationTypeEnum.WEB_CRAWL, 7), (OperationTypeEnum.WEB_NEWS, 8), (OperationTypeEnum.WEB_QUESTIONS, 9) ), version="sonar", calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.005 + (bytesReceived / 4 / 1000) * 0.005 ), AiModel( name="sonar-pro", displayName="Perplexity Sonar Pro", connectorType="perplexity", apiUrl="https://api.perplexity.ai/chat/completions", temperature=0.2, maxTokens=4000, contextLength=32000, costPer1kTokensInput=0.01, costPer1kTokensOutput=0.01, speedRating=6, # Slower due to AI analysis qualityRating=10, # Best AI analysis quality # capabilities removed (not used in business logic) functionCall=self.callWebOperation, priority=PriorityEnum.QUALITY, processingMode=ProcessingModeEnum.DETAILED, operationTypes=createOperationTypeRatings( (OperationTypeEnum.WEB_RESEARCH, 10), (OperationTypeEnum.WEB_SEARCH, 9), (OperationTypeEnum.WEB_CRAWL, 8), (OperationTypeEnum.WEB_NEWS, 8), (OperationTypeEnum.WEB_QUESTIONS, 9) ), version="sonar-pro", calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.01 + (bytesReceived / 4 / 1000) * 0.01 ) ] async def callAiBasic(self, modelCall: AiModelCall) -> AiModelResponse: """ Calls the Perplexity API with the given messages using standardized pattern. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with content and metadata Raises: HTTPException: For errors in API communication """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens payload = { "model": model.name, "messages": messages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity API error: {response.status_code} - {response.text}" logger.error(error_detail) # Provide more specific error messages based on status code if response.status_code == 429: error_message = "Rate limit exceeded. Please wait before making another request." elif response.status_code == 401: error_message = "Invalid API key. Please check your Perplexity API configuration." elif response.status_code == 400: error_message = f"Invalid request to Perplexity API: {response.text}" else: error_message = f"Perplexity API error ({response.status_code}): {response.text}" raise HTTPException(status_code=500, detail=error_message) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error calling Perplexity API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling Perplexity API: {str(e)}") async def callAiWithWebSearch(self, modelCall: AiModelCall) -> AiModelResponse: """ Calls Perplexity API with web search capabilities for research using standardized pattern. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with content and metadata """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens # Parse unified prompt JSON format promptContent = messages[0]["content"] if messages else "" import json promptData = json.loads(promptContent) # Create a more specific prompt for Perplexity based on the unified format searchPrompt = promptData.get("searchPrompt", promptContent) maxResults = promptData.get("maxResults", 5) timeRange = promptData.get("timeRange") country = promptData.get("country") language = promptData.get("language") # Create enhanced prompt for Perplexity enhancedPrompt = f"""Search the web for: {searchPrompt} Please provide a comprehensive response with relevant URLs and information. Focus on finding {maxResults} most relevant results. {f"Limit results to the last {timeRange}" if timeRange else ""} {f"Focus on {country}" if country else ""} {f"Provide results in {language}" if language else ""} Please format your response as a JSON object with the following structure: {{ "query": "{searchPrompt}", "results": [ {{ "title": "Result title", "url": "https://example.com", "content": "Brief description or excerpt" }} ], "total_count": number_of_results }} Include actual URLs in your response.""" # Update the messages with the enhanced prompt enhancedMessages = [{"role": "user", "content": enhancedPrompt}] payload = { "model": model.name, "messages": enhancedMessages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity Web Search API error: {response.status_code} - {response.text}" logger.error(error_detail) if response.status_code == 429: error_message = "Rate limit exceeded for web search. Please wait before making another request." elif response.status_code == 401: error_message = "Invalid API key for web search. Please check your Perplexity API configuration." elif response.status_code == 400: error_message = f"Invalid request to Perplexity Web Search API: {response.text}" else: error_message = f"Perplexity Web Search API error ({response.status_code}): {response.text}" raise HTTPException(status_code=500, detail=error_message) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error calling Perplexity Web Search API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling Perplexity Web Search API: {str(e)}") async def researchTopic(self, modelCall: AiModelCall) -> AiModelResponse: """ Research a topic using Perplexity's web search capabilities using standardized pattern. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with research content """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens payload = { "model": model.name, "messages": messages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity Research API error: {response.status_code} - {response.text}" logger.error(error_detail) if response.status_code == 429: error_message = "Rate limit exceeded for research. Please wait before making another request." elif response.status_code == 401: error_message = "Invalid API key for research. Please check your Perplexity API configuration." elif response.status_code == 400: error_message = f"Invalid request to Perplexity Research API: {response.text}" else: error_message = f"Perplexity Research API error ({response.status_code}): {response.text}" raise HTTPException(status_code=500, detail=error_message) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error researching topic: {str(e)}") raise HTTPException(status_code=500, detail=f"Error researching topic: {str(e)}") async def answerQuestion(self, modelCall: AiModelCall) -> AiModelResponse: """ Answer a question using web search for current information using standardized pattern. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with answer content """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens payload = { "model": model.name, "messages": messages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity Q&A API error: {response.status_code} - {response.text}" logger.error(error_detail) if response.status_code == 429: error_message = "Rate limit exceeded for Q&A. Please wait before making another request." elif response.status_code == 401: error_message = "Invalid API key for Q&A. Please check your Perplexity API configuration." elif response.status_code == 400: error_message = f"Invalid request to Perplexity Q&A API: {response.text}" else: error_message = f"Perplexity Q&A API error ({response.status_code}): {response.text}" raise HTTPException(status_code=500, detail=error_message) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error answering question: {str(e)}") raise HTTPException(status_code=500, detail=f"Error answering question: {str(e)}") async def getCurrentNews(self, modelCall: AiModelCall) -> AiModelResponse: """ Get current news on a specific topic using standardized pattern. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with news content """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens payload = { "model": model.name, "messages": messages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity News API error: {response.status_code} - {response.text}" logger.error(error_detail) if response.status_code == 429: error_message = "Rate limit exceeded for news. Please wait before making another request." elif response.status_code == 401: error_message = "Invalid API key for news. Please check your Perplexity API configuration." elif response.status_code == 400: error_message = f"Invalid request to Perplexity News API: {response.text}" else: error_message = f"Perplexity News API error ({response.status_code}): {response.text}" raise HTTPException(status_code=500, detail=error_message) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error getting current news: {str(e)}") raise HTTPException(status_code=500, detail=f"Error getting current news: {str(e)}") async def crawl(self, modelCall: AiModelCall) -> AiModelResponse: """ Crawl URLs using Perplexity's web search capabilities for content extraction. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with content and metadata """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens # Parse unified prompt JSON format promptContent = messages[0]["content"] if messages else "" import json promptData = json.loads(promptContent) # Extract parameters from unified prompt JSON urls = promptData.get("urls", []) extractDepth = promptData.get("extractDepth", "advanced") formatType = promptData.get("format", "markdown") if not urls: return AiModelResponse( content="No URLs provided for crawling", success=False, error="No URLs found in prompt data" ) # Create enhanced prompt for Perplexity to crawl URLs urlsList = ", ".join(urls) enhancedPrompt = f"""Please extract and analyze content from these URLs: {urlsList} Extraction requirements: - Extract depth: {extractDepth} - Output format: {formatType} - Focus on main content, not navigation or ads - Preserve important structure and formatting Please format your response as a JSON object with the following structure: {{ "urls": {json.dumps(urls)}, "results": [ {{ "url": "https://example.com", "title": "Page title", "content": "Extracted content in {formatType} format", "extractedAt": "2024-01-01T00:00:00Z" }} ], "total_count": number_of_urls_processed }} Extract content from each URL and provide detailed analysis.""" # Update the messages with the enhanced prompt enhancedMessages = [{"role": "user", "content": enhancedPrompt}] payload = { "model": model.name, "messages": enhancedMessages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity Crawl API error: {response.status_code} - {response.text}" logger.error(error_detail) if response.status_code == 429: error_message = "Rate limit exceeded for crawl. Please wait before making another request." elif response.status_code == 401: error_message = "Invalid API key for crawl. Please check your Perplexity API configuration." elif response.status_code == 400: error_message = f"Invalid request to Perplexity Crawl API: {response.text}" else: error_message = f"Perplexity Crawl API error ({response.status_code}): {response.text}" raise HTTPException(status_code=500, detail=error_message) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error calling Perplexity Crawl API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling Perplexity Crawl API: {str(e)}") async def callWebOperation(self, modelCall: AiModelCall) -> AiModelResponse: """ Universal web operation handler that distributes to the correct method based on the operationType from AiCallOptions. """ try: options = modelCall.options operationType = getattr(options, "operationType", None) if operationType == OperationTypeEnum.WEB_SEARCH: return await self.callAiWithWebSearch(modelCall) elif operationType == OperationTypeEnum.WEB_CRAWL: return await self.crawl(modelCall) elif operationType == OperationTypeEnum.WEB_RESEARCH: return await self.research(modelCall) elif operationType == OperationTypeEnum.WEB_QUESTIONS: return await self.questions(modelCall) elif operationType == OperationTypeEnum.WEB_NEWS: return await self.news(modelCall) else: # Fallback to research for unknown operation types return await self.research(modelCall) except Exception as e: return AiModelResponse( content="", success=False, error=str(e) ) async def research(self, modelCall: AiModelCall) -> AiModelResponse: """ Research topics using Perplexity's web search capabilities. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with research content and metadata """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens # Parse unified prompt JSON format promptContent = messages[0]["content"] if messages else "" import json promptData = json.loads(promptContent) # Extract parameters from unified prompt JSON researchPrompt = promptData.get("researchPrompt", promptContent) maxResults = promptData.get("maxResults", 8) timeRange = promptData.get("timeRange") country = promptData.get("country") language = promptData.get("language") # Create enhanced prompt for research enhancedPrompt = f"""Conduct comprehensive research on: {researchPrompt} Research requirements: - Provide detailed analysis and insights - Include multiple perspectives and sources - Focus on finding {maxResults} most relevant sources {f"Limit results to the last {timeRange}" if timeRange else ""} {f"Focus on {country}" if country else ""} {f"Provide results in {language}" if language else ""} Please format your response as a JSON object with the following structure: {{ "query": "{researchPrompt}", "research_results": [ {{ "title": "Source title", "url": "https://example.com", "summary": "Brief summary", "content": "Detailed content", "extractedAt": "2024-01-01T00:00:00Z" }} ], "total_count": number_of_sources, "operation_type": "research" }} Provide comprehensive research with detailed analysis.""" # Update the messages with the enhanced prompt enhancedMessages = [{"role": "user", "content": enhancedPrompt}] payload = { "model": model.name, "messages": enhancedMessages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity Research API error: {response.status_code} - {response.text}" logger.error(error_detail) raise HTTPException(status_code=500, detail=error_detail) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error calling Perplexity Research API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling Perplexity Research API: {str(e)}") async def questions(self, modelCall: AiModelCall) -> AiModelResponse: """ Answer questions using Perplexity's web search capabilities. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with answer and supporting sources """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens # Parse unified prompt JSON format promptContent = messages[0]["content"] if messages else "" import json promptData = json.loads(promptContent) # Extract parameters from unified prompt JSON question = promptData.get("question", promptContent) context = promptData.get("context", "") maxResults = promptData.get("maxResults", 6) timeRange = promptData.get("timeRange") country = promptData.get("country") language = promptData.get("language") # Create enhanced prompt for questions contextText = f"\nAdditional context: {context}" if context else "" enhancedPrompt = f"""Answer this question using web research: {question}{contextText} Answer requirements: - Provide a comprehensive answer with supporting evidence - Include {maxResults} most relevant sources - Cite sources with URLs {f"Focus on recent information (last {timeRange})" if timeRange else ""} {f"Focus on {country}" if country else ""} {f"Provide answer in {language}" if language else ""} Please format your response as a JSON object with the following structure: {{ "question": "{question}", "answer": "Comprehensive answer to the question", "answer_sources": [ {{ "title": "Source title", "url": "https://example.com", "summary": "Brief summary", "content": "Relevant content excerpt", "relevance": "Why this source is relevant" }} ], "total_count": number_of_sources, "operation_type": "questions" }} Provide a detailed answer with well-cited sources.""" # Update the messages with the enhanced prompt enhancedMessages = [{"role": "user", "content": enhancedPrompt}] payload = { "model": model.name, "messages": enhancedMessages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity Questions API error: {response.status_code} - {response.text}" logger.error(error_detail) raise HTTPException(status_code=500, detail=error_detail) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error calling Perplexity Questions API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling Perplexity Questions API: {str(e)}") async def news(self, modelCall: AiModelCall) -> AiModelResponse: """ Search and analyze news using Perplexity's web search capabilities. Args: modelCall: AiModelCall with messages and options Returns: AiModelResponse with news articles and analysis """ try: # Extract parameters from modelCall messages = modelCall.messages model = modelCall.model options = modelCall.options temperature = getattr(options, "temperature", None) if temperature is None: temperature = model.temperature maxTokens = model.maxTokens # Parse unified prompt JSON format promptContent = messages[0]["content"] if messages else "" import json promptData = json.loads(promptContent) # Extract parameters from unified prompt JSON newsPrompt = promptData.get("newsPrompt", promptContent) maxResults = promptData.get("maxResults", 10) timeRange = promptData.get("timeRange", "w") # Default to week for news country = promptData.get("country") language = promptData.get("language") # Create enhanced prompt for news enhancedPrompt = f"""Find and analyze recent news about: {newsPrompt} News requirements: - Find {maxResults} most recent and relevant news articles - Focus on the last {timeRange} (recent news) - Include diverse sources and perspectives {f"Focus on news from {country}" if country else ""} {f"Provide news in {language}" if language else ""} Please format your response as a JSON object with the following structure: {{ "news_query": "{newsPrompt}", "articles": [ {{ "title": "Article title", "url": "https://example.com", "content": "Article content", "date": "2024-01-01", "source": "News source name", "summary": "Brief summary of the article" }} ], "total_count": number_of_articles, "operation_type": "news" }} Provide comprehensive news coverage with analysis.""" # Update the messages with the enhanced prompt enhancedMessages = [{"role": "user", "content": enhancedPrompt}] payload = { "model": model.name, "messages": enhancedMessages, "temperature": temperature, "max_tokens": maxTokens } response = await self.httpClient.post( model.apiUrl, json=payload ) if response.status_code != 200: error_detail = f"Perplexity News API error: {response.status_code} - {response.text}" logger.error(error_detail) raise HTTPException(status_code=500, detail=error_detail) responseJson = response.json() content = responseJson["choices"][0]["message"]["content"] return AiModelResponse( content=content, success=True, modelId=model.name, metadata={"response_id": responseJson.get("id", "")} ) except Exception as e: logger.error(f"Error calling Perplexity News API: {str(e)}") raise HTTPException(status_code=500, detail=f"Error calling Perplexity News API: {str(e)}") async def _testConnection(self) -> bool: """ Tests the connection to the Perplexity API. Returns: True if connection is successful, False otherwise """ try: # Try a simple test message testMessages = [ {"role": "user", "content": "Hello, please respond with just 'OK' to confirm the connection works."} ] response = await self.callAiBasic(testMessages) return response and len(response.strip()) > 0 except Exception as e: logger.error(f"Perplexity connection test failed: {str(e)}") return False