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="llama-3.1-sonar-large-128k-online", displayName="Perplexity Llama 3.1 Sonar Large 128k", connectorType="perplexity", apiUrl="https://api.perplexity.ai/chat/completions", temperature=0.2, maxTokens=128000, contextLength=128000, costPer1kTokensInput=0.005, costPer1kTokensOutput=0.005, speedRating=8, qualityRating=8, # capabilities removed (not used in business logic) functionCall=self.callAiBasic, priority=PriorityEnum.BALANCED, processingMode=ProcessingModeEnum.ADVANCED, operationTypes=createOperationTypeRatings( (OperationTypeEnum.PLAN, 7), (OperationTypeEnum.DATA_ANALYSE, 8), (OperationTypeEnum.DATA_GENERATE, 7) ), version="llama-3.1-sonar-large-128k-online", 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=128000, contextLength=128000, 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.callAiWithWebSearch, 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 ), AiModel( name="mistral-7b-instruct", displayName="Perplexity Mistral 7B Instruct", connectorType="perplexity", apiUrl="https://api.perplexity.ai/chat/completions", temperature=0.2, maxTokens=32000, contextLength=32000, costPer1kTokensInput=0.002, costPer1kTokensOutput=0.002, speedRating=9, # Fast for basic AI tasks qualityRating=7, # Good but not premium quality # capabilities removed (not used in business logic) functionCall=self.researchTopic, priority=PriorityEnum.COST, processingMode=ProcessingModeEnum.BASIC, operationTypes=createOperationTypeRatings( (OperationTypeEnum.WEB_RESEARCH, 7), (OperationTypeEnum.WEB_SEARCH, 6), (OperationTypeEnum.WEB_CRAWL, 5), (OperationTypeEnum.WEB_NEWS, 5), (OperationTypeEnum.WEB_QUESTIONS, 6) ), version="mistral-7b-instruct", calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002 ), AiModel( name="mistral-7b-instruct-qa", displayName="Perplexity Mistral 7B Instruct QA", connectorType="perplexity", apiUrl="https://api.perplexity.ai/chat/completions", temperature=0.2, maxTokens=32000, contextLength=32000, costPer1kTokensInput=0.002, costPer1kTokensOutput=0.002, speedRating=9, # Fast for Q&A tasks qualityRating=7, # Good but not premium quality # capabilities removed (not used in business logic) functionCall=self.answerQuestion, priority=PriorityEnum.COST, processingMode=ProcessingModeEnum.BASIC, operationTypes=createOperationTypeRatings( (OperationTypeEnum.WEB_RESEARCH, 6), (OperationTypeEnum.WEB_SEARCH, 5), (OperationTypeEnum.WEB_CRAWL, 4), (OperationTypeEnum.WEB_NEWS, 4), (OperationTypeEnum.WEB_QUESTIONS, 10) ), version="mistral-7b-instruct", calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002 ), AiModel( name="mistral-7b-instruct-news", displayName="Perplexity Mistral 7B Instruct News", connectorType="perplexity", apiUrl="https://api.perplexity.ai/chat/completions", temperature=0.2, maxTokens=32000, contextLength=32000, costPer1kTokensInput=0.002, costPer1kTokensOutput=0.002, speedRating=9, # Fast for news tasks qualityRating=7, # Good but not premium quality # capabilities removed (not used in business logic) functionCall=self.getCurrentNews, priority=PriorityEnum.COST, processingMode=ProcessingModeEnum.BASIC, operationTypes=createOperationTypeRatings( (OperationTypeEnum.WEB_RESEARCH, 6), (OperationTypeEnum.WEB_SEARCH, 5), (OperationTypeEnum.WEB_CRAWL, 4), (OperationTypeEnum.WEB_NEWS, 10), (OperationTypeEnum.WEB_QUESTIONS, 4) ), version="mistral-7b-instruct", calculatePriceUsd=lambda processingTime, bytesSent, bytesReceived: (bytesSent / 4 / 1000) * 0.002 + (bytesReceived / 4 / 1000) * 0.002 ) ] 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 = options.get("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 = options.get("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 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 = options.get("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 = options.get("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 = options.get("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 _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