File size: 14,223 Bytes
c922f8b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
"""
Perplexity API tool for the GAIA agent.

This module provides a tool for querying the Perplexity API with advanced models:
- sonar-reasoning: Balanced model for general reasoning and search tasks
- sonar-reasoning-pro: Enhanced version with stronger analytical capabilities
- sonar-deep-research: Specialized model for in-depth research and complex queries

The tool uses a fixed temperature of 0.1 for maximum accuracy and allows selecting
the appropriate model for different types of tasks.

The tool handles API responses and errors, and extracts content and citations from responses.

Models Reference:
- sonar-reasoning: Balanced accuracy and speed for general queries
- sonar-reasoning-pro: Higher accuracy, deeper analysis, suitable for complex reasoning tasks
- sonar-deep-research: Maximum depth and research capabilities, ideal for scholarly research
"""

import os
import json
import logging
import urllib.request
import urllib.error
from typing import Dict, Any, List, Optional, Union

logger = logging.getLogger("gaia_agent.tools.perplexity")

if not logger.handlers:
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)

class PerplexityTool:
    """Tool for querying the Perplexity API."""
    
    # Valid models supported by the Perplexity API
    VALID_MODELS = ["sonar", "sonar-reasoning", "sonar-reasoning-pro", "sonar-deep-research"]
    
    def __init__(self, config: Optional[Dict[str, Any]] = None):
        """
        Initialize the Perplexity API tool.
        
        Args:
            config: Optional configuration dictionary that may include:
                - api_key: Perplexity API key (otherwise read from environment)
                - model: Model to use (default: "sonar-reasoning")
                - temperature: Temperature setting (default: 0.1)
                - timeout: Timeout in seconds (default: 30)
        """
        self.config = config or {}
        
        self.api_key = self.config.get("api_key") or os.getenv("PERPLEXITY_API_KEY")
        if not self.api_key:
            logger.warning("Perplexity API key not found in config or environment variables")
        
        self.api_url = "https://api.perplexity.ai"
        
        # Get model from config, environment variable, or use default
        model = self.config.get("model") or os.getenv("PERPLEXITY_MODEL", "sonar-reasoning")
        if model not in self.VALID_MODELS:
            logger.warning(f"Invalid model '{model}'. Using default 'sonar-reasoning' instead.")
            model = "sonar-reasoning"
        self.model = model
        
        # Set temperature to 0.1 for maximum accuracy unless otherwise specified
        self.temperature = self.config.get("temperature", 0.1)
        self.timeout = self.config.get("timeout", 30)
        
        self.endpoint = f"{self.api_url}/chat/completions"
        
        logger.info(f"Initialized Perplexity tool with model: {self.model}")
        
    
    def query(self,
               question: str,
               max_tokens: int = 500,
               system_prompt: Optional[str] = None,
               model: Optional[str] = None) -> Dict[str, Any]:
        """
        Send a query to the Perplexity API.
        
        Args:
            question: The question to ask
            max_tokens: Maximum number of tokens in the response
            system_prompt: Optional system prompt
            model: Override the model for this specific query
            
        Returns:
            Dictionary containing the query results
            
        Raises:
            Exception: If an error occurs during the query
        """
        
        if not self.api_key:
            raise Exception("Perplexity API key not available")
        
        try:
            # Determine which model to use (either the override or the default)
            use_model = model if model else self.model
            
            # Validate the model
            if use_model not in self.VALID_MODELS:
                logger.warning(f"Invalid model specified: '{use_model}'. Using '{self.model}' instead.")
                use_model = self.model
            
            messages = []
            
            if system_prompt:
                messages.append({"role": "system", "content": system_prompt})
                
            messages.append({"role": "user", "content": question})
            
            # Model parameter must be included in the request body
            payload = {
                "model": use_model,
                "messages": messages,
                "max_tokens": max_tokens,
                "temperature": self.temperature
            }
            
            logger.info(f"Querying Perplexity API with model: {use_model}")
            
            payload_bytes = json.dumps(payload).encode('utf-8')
            
            headers = {
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json"
            }
            
            req = urllib.request.Request(
                self.endpoint,
                data=payload_bytes,
                headers=headers,
                method="POST"
            )
            
            with urllib.request.urlopen(req, timeout=self.timeout) as response:
                response_data = response.read().decode('utf-8')
                api_response = json.loads(response_data)
                
                content = self._extract_content(api_response)
                citations = self._extract_citations(api_response)
                usage = self._get_usage(api_response)
                
                result = {
                    "query": question,
                    "content": content,
                    "citations": citations,
                    "usage": usage,
                    "model": use_model,  # Use the actual model used for the query
                    "raw_response": api_response
                }
                
                
                return result
                
        except urllib.error.HTTPError as e:
            logger.error(f"HTTP Error {e.code} when querying Perplexity API: {e.reason}")
            try:
                error_data = e.read().decode('utf-8')
                logger.error(f"Error details: {error_data}")
            except:
                logger.error("Could not read error details")
            raise Exception(f"Perplexity API HTTP error: {e.code} {e.reason}")
            
        except urllib.error.URLError as e:
            logger.error(f"URL Error when querying Perplexity API: {e.reason}")
            raise Exception(f"Perplexity API connection error: {e.reason}")
            
        except Exception as e:
            logger.error(f"Error querying Perplexity API: {str(e)}")
            raise Exception(f"Perplexity API error: {str(e)}")
    
    def search(self,
               query: str,
               max_tokens: int = 500,
               model: Optional[str] = None,
               depth: str = "standard") -> Dict[str, Any]:
        """
        Search the web using the Perplexity API.
        
        Args:
            query: The search query
            max_tokens: Maximum number of tokens in the response
            model: Optional model override (uses instance model by default)
            depth: Research depth - "standard", "deep", or "comprehensive"
            
        Returns:
            Dictionary containing the search results
            
        Raises:
            Exception: If an error occurs during the search
        """
        # Select the appropriate model based on depth if not explicitly specified
        if not model:
            if depth == "deep":
                model = "sonar-reasoning-pro"
            elif depth == "comprehensive":
                model = "sonar-deep-research"
        
        # Adjust the system prompt based on the requested depth
        if depth == "standard":
            system_prompt = """You are a helpful web search assistant.
            Search the web for accurate and up-to-date information to answer the user's query.
            Provide a comprehensive answer with relevant facts and details.
            Include citations to your sources."""
        elif depth == "deep":
            system_prompt = """You are an advanced web search and analysis assistant.
            Perform a deep search for accurate, detailed, and up-to-date information to answer the user's query.
            Analyze the information critically, identifying key patterns, insights, and connections.
            Provide a thorough answer with comprehensive analysis.
            Include citations to your sources and note the reliability of each source."""
        elif depth == "comprehensive":
            system_prompt = """You are an expert research assistant conducting comprehensive analysis.
            Perform exhaustive research on the user's query, finding detailed information from diverse sources.
            Critically analyze all information, evaluating source credibility and identifying consensus vs. disagreements.
            Synthesize findings into a comprehensive research summary addressing all aspects of the query.
            Include thorough citations to your sources, noting the authority and reliability of each source.
            Identify any gaps in available information or areas where further research would be beneficial."""
        else:
            # Default to standard if depth is not recognized
            system_prompt = """You are a helpful web search assistant.
            Search the web for accurate and up-to-date information to answer the user's query.
            Provide a comprehensive answer with relevant facts and details.
            Include citations to your sources."""
        
        logger.info(f"Performing {depth} search with model: {model or self.model}")
        return self.query(query, max_tokens, system_prompt, model)
    
    def _extract_content(self, response: Dict[str, Any]) -> Optional[str]:
        """
        Extract content from a Perplexity API response.
        
        Args:
            response: The API response dictionary
            
        Returns:
            The extracted content, or None if not found
        """
        if "choices" in response and len(response["choices"]) > 0:
            if "message" in response["choices"][0]:
                return response["choices"][0]["message"].get("content")
        return None
    
    def _extract_citations(self, response: Dict[str, Any]) -> List[str]:
        """
        Extract citations from a Perplexity API response.
        
        Args:
            response: The API response dictionary
            
        Returns:
            List of citation URLs
        """
        return response.get("citations", [])
    
    def _get_usage(self, response: Dict[str, Any]) -> Dict[str, Any]:
        """
        Extract usage information from a Perplexity API response.
        
        Args:
            response: The API response dictionary
            
        Returns:
            Dictionary containing usage information
        """
        return response.get("usage", {})


def create_perplexity_tool(model: str = "sonar-reasoning") -> PerplexityTool:
    """
    Create a Perplexity tool instance with the specified model.
    
    Args:
        model: The Perplexity model to use. Options:
            - "sonar-reasoning": Balanced model for general reasoning and search (default)
            - "sonar-reasoning-pro": Enhanced model with stronger analytical capabilities
            - "sonar-deep-research": Specialized model for in-depth research and complex queries
    
    Returns:
        A configured PerplexityTool instance
    """
    config = {"model": model}
    return PerplexityTool(config)


def create_perplexity_tool_pro() -> PerplexityTool:
    """
    Create a Perplexity tool instance using the sonar-reasoning-pro model.
    
    This model provides enhanced analysis capabilities for complex reasoning tasks.
    
    Returns:
        A PerplexityTool instance configured with the sonar-reasoning-pro model
    """
    return create_perplexity_tool("sonar-reasoning-pro")


def create_perplexity_tool_research() -> PerplexityTool:
    """
    Create a Perplexity tool instance using the sonar-deep-research model.
    
    This model provides maximum depth and research capabilities, ideal for
    scholarly research and complex investigative queries.
    
    Returns:
        A PerplexityTool instance configured with the sonar-deep-research model
    """
    return create_perplexity_tool("sonar-deep-research")


# Additional specialized search methods

def deep_search(query: str, max_tokens: int = 750) -> Dict[str, Any]:
    """
    Perform a deep search using the sonar-reasoning-pro model.
    
    This is a convenience function for performing a search with enhanced depth
    and analysis. It uses the sonar-reasoning-pro model which offers improved
    analytical capabilities compared to the standard model.
    
    Args:
        query: The search query
        max_tokens: Maximum number of tokens in the response (default: 750)
        
    Returns:
        Dictionary containing the search results with enhanced analysis
    """
    tool = create_perplexity_tool_pro()
    return tool.search(query, max_tokens, depth="deep")


def comprehensive_research(query: str, max_tokens: int = 1000) -> Dict[str, Any]:
    """
    Perform comprehensive research using the sonar-deep-research model.
    
    This is a convenience function for performing exhaustive research on complex topics.
    It uses the sonar-deep-research model which provides the maximum depth and
    research capabilities available.
    
    Args:
        query: The research query
        max_tokens: Maximum number of tokens in the response (default: 1000)
        
    Returns:
        Dictionary containing thorough research results with comprehensive analysis
    """
    tool = create_perplexity_tool_research()
    return tool.search(query, max_tokens, depth="comprehensive")