rig/providers/
perplexity.rs

1//! Perplexity API client and Rig integration
2//!
3//! # Example
4//! ```
5//! use rig::providers::perplexity;
6//!
7//! let client = perplexity::Client::new("YOUR_API_KEY");
8//!
9//! let llama_3_1_sonar_small_online = client.completion_model(perplexity::LLAMA_3_1_SONAR_SMALL_ONLINE);
10//! ```
11
12use crate::{
13    agent::AgentBuilder,
14    completion::{self, message, CompletionError, MessageError},
15    extractor::ExtractorBuilder,
16    json_utils, OneOrMany,
17};
18
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use serde_json::json;
22
23// ================================================================
24// Main Cohere Client
25// ================================================================
26const PERPLEXITY_API_BASE_URL: &str = "https://api.perplexity.ai";
27
28#[derive(Clone)]
29pub struct Client {
30    base_url: String,
31    http_client: reqwest::Client,
32}
33
34impl Client {
35    pub fn new(api_key: &str) -> Self {
36        Self::from_url(api_key, PERPLEXITY_API_BASE_URL)
37    }
38
39    /// Create a new Perplexity client from the `PERPLEXITY_API_KEY` environment variable.
40    /// Panics if the environment variable is not set.
41    pub fn from_env() -> Self {
42        let api_key = std::env::var("PERPLEXITY_API_KEY").expect("PERPLEXITY_API_KEY not set");
43        Self::new(&api_key)
44    }
45
46    pub fn from_url(api_key: &str, base_url: &str) -> Self {
47        Self {
48            base_url: base_url.to_string(),
49            http_client: reqwest::Client::builder()
50                .default_headers({
51                    let mut headers = reqwest::header::HeaderMap::new();
52                    headers.insert(
53                        "Authorization",
54                        format!("Bearer {}", api_key)
55                            .parse()
56                            .expect("Bearer token should parse"),
57                    );
58                    headers
59                })
60                .build()
61                .expect("Perplexity reqwest client should build"),
62        }
63    }
64
65    pub fn post(&self, path: &str) -> reqwest::RequestBuilder {
66        let url = format!("{}/{}", self.base_url, path).replace("//", "/");
67        self.http_client.post(url)
68    }
69
70    pub fn completion_model(&self, model: &str) -> CompletionModel {
71        CompletionModel::new(self.clone(), model)
72    }
73
74    pub fn agent(&self, model: &str) -> AgentBuilder<CompletionModel> {
75        AgentBuilder::new(self.completion_model(model))
76    }
77
78    pub fn extractor<T: JsonSchema + for<'a> Deserialize<'a> + Serialize + Send + Sync>(
79        &self,
80        model: &str,
81    ) -> ExtractorBuilder<T, CompletionModel> {
82        ExtractorBuilder::new(self.completion_model(model))
83    }
84}
85
86#[derive(Debug, Deserialize)]
87struct ApiErrorResponse {
88    message: String,
89}
90
91#[derive(Debug, Deserialize)]
92#[serde(untagged)]
93enum ApiResponse<T> {
94    Ok(T),
95    Err(ApiErrorResponse),
96}
97
98// ================================================================
99// Perplexity Completion API
100// ================================================================
101/// `sonar-pro` completion model
102pub const SONAR_PRO: &str = "sonar-pro";
103/// `sonar` completion model
104pub const SONAR: &str = "sonar";
105
106#[derive(Debug, Deserialize)]
107pub struct CompletionResponse {
108    pub id: String,
109    pub model: String,
110    pub object: String,
111    pub created: u64,
112    #[serde(default)]
113    pub choices: Vec<Choice>,
114    pub usage: Usage,
115}
116
117#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
118pub struct Message {
119    pub role: Role,
120    pub content: String,
121}
122
123#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
124#[serde(rename_all = "lowercase")]
125pub enum Role {
126    System,
127    User,
128    Assistant,
129}
130
131#[derive(Deserialize, Debug)]
132pub struct Delta {
133    pub role: Role,
134    pub content: String,
135}
136
137#[derive(Deserialize, Debug)]
138pub struct Choice {
139    pub index: usize,
140    pub finish_reason: String,
141    pub message: Message,
142    pub delta: Delta,
143}
144
145#[derive(Deserialize, Debug)]
146pub struct Usage {
147    pub prompt_tokens: u32,
148    pub completion_tokens: u32,
149    pub total_tokens: u32,
150}
151
152impl std::fmt::Display for Usage {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        write!(
155            f,
156            "Prompt tokens: {}\nCompletion tokens: {} Total tokens: {}",
157            self.prompt_tokens, self.completion_tokens, self.total_tokens
158        )
159    }
160}
161
162impl TryFrom<CompletionResponse> for completion::CompletionResponse<CompletionResponse> {
163    type Error = CompletionError;
164
165    fn try_from(response: CompletionResponse) -> Result<Self, Self::Error> {
166        let choice = response.choices.first().ok_or_else(|| {
167            CompletionError::ResponseError("Response contained no choices".to_owned())
168        })?;
169
170        match &choice.message {
171            Message {
172                role: Role::Assistant,
173                content,
174            } => Ok(completion::CompletionResponse {
175                choice: OneOrMany::one(content.clone().into()),
176                raw_response: response,
177            }),
178            _ => Err(CompletionError::ResponseError(
179                "Response contained no assistant message".to_owned(),
180            )),
181        }
182    }
183}
184
185#[derive(Clone)]
186pub struct CompletionModel {
187    client: Client,
188    pub model: String,
189}
190
191impl CompletionModel {
192    pub fn new(client: Client, model: &str) -> Self {
193        Self {
194            client,
195            model: model.to_string(),
196        }
197    }
198}
199
200impl TryFrom<message::Message> for Message {
201    type Error = MessageError;
202
203    fn try_from(message: message::Message) -> Result<Self, Self::Error> {
204        Ok(match message {
205            message::Message::User { content } => {
206                let collapsed_content = content
207                    .into_iter()
208                    .map(|content| match content {
209                        message::UserContent::Text(message::Text { text }) => Ok(text),
210                        _ => Err(MessageError::ConversionError(
211                            "Only text content is supported by Perplexity".to_owned(),
212                        )),
213                    })
214                    .collect::<Result<Vec<_>, _>>()?
215                    .join("\n");
216
217                Message {
218                    role: Role::User,
219                    content: collapsed_content,
220                }
221            }
222
223            message::Message::Assistant { content } => {
224                let collapsed_content = content
225                    .into_iter()
226                    .map(|content| {
227                        Ok(match content {
228                            message::AssistantContent::Text(message::Text { text }) => text,
229                            _ => return Err(MessageError::ConversionError(
230                                "Only text assistant message content is supported by Perplexity"
231                                    .to_owned(),
232                            )),
233                        })
234                    })
235                    .collect::<Result<Vec<_>, _>>()?
236                    .join("\n");
237
238                Message {
239                    role: Role::Assistant,
240                    content: collapsed_content,
241                }
242            }
243        })
244    }
245}
246
247impl From<Message> for message::Message {
248    fn from(message: Message) -> Self {
249        match message.role {
250            Role::User => message::Message::user(message.content),
251            Role::Assistant => message::Message::assistant(message.content),
252
253            // System messages get coerced into user messages for ease of error handling.
254            // They should be handled on the outside of `Message` conversions via the preamble.
255            Role::System => message::Message::user(message.content),
256        }
257    }
258}
259
260impl completion::CompletionModel for CompletionModel {
261    type Response = CompletionResponse;
262
263    #[cfg_attr(feature = "worker", worker::send)]
264    async fn completion(
265        &self,
266        completion_request: completion::CompletionRequest,
267    ) -> Result<completion::CompletionResponse<CompletionResponse>, CompletionError> {
268        // Add context documents to current prompt
269        let prompt_with_context = completion_request.prompt_with_context();
270
271        // Add preamble to messages (if available)
272        let mut messages: Vec<Message> = if let Some(preamble) = completion_request.preamble {
273            vec![Message {
274                role: Role::System,
275                content: preamble,
276            }]
277        } else {
278            vec![]
279        };
280
281        // Add chat history to messages
282        for message in completion_request.chat_history {
283            messages.push(
284                message
285                    .try_into()
286                    .map_err(|e: MessageError| CompletionError::RequestError(e.into()))?,
287            );
288        }
289
290        // Add user prompt to messages
291        messages.push(
292            prompt_with_context
293                .try_into()
294                .map_err(|e: MessageError| CompletionError::RequestError(e.into()))?,
295        );
296
297        // Compose request
298        let request = json!({
299            "model": self.model,
300            "messages": messages,
301            "temperature": completion_request.temperature,
302        });
303
304        let response = self
305            .client
306            .post("/chat/completions")
307            .json(
308                &if let Some(ref params) = completion_request.additional_params {
309                    json_utils::merge(request.clone(), params.clone())
310                } else {
311                    request.clone()
312                },
313            )
314            .send()
315            .await?;
316
317        if response.status().is_success() {
318            match response.json::<ApiResponse<CompletionResponse>>().await? {
319                ApiResponse::Ok(completion) => {
320                    tracing::info!(target: "rig",
321                        "Perplexity completion token usage: {}",
322                        completion.usage
323                    );
324                    Ok(completion.try_into()?)
325                }
326                ApiResponse::Err(error) => Err(CompletionError::ProviderError(error.message)),
327            }
328        } else {
329            Err(CompletionError::ProviderError(response.text().await?))
330        }
331    }
332}
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_deserialize_message() {
339        let json_data = r#"
340        {
341            "role": "user",
342            "content": "Hello, how can I help you?"
343        }
344        "#;
345
346        let message: Message = serde_json::from_str(json_data).unwrap();
347        assert_eq!(message.role, Role::User);
348        assert_eq!(message.content, "Hello, how can I help you?");
349    }
350
351    #[test]
352    fn test_serialize_message() {
353        let message = Message {
354            role: Role::Assistant,
355            content: "I am here to assist you.".to_string(),
356        };
357
358        let json_data = serde_json::to_string(&message).unwrap();
359        let expected_json = r#"{"role":"assistant","content":"I am here to assist you."}"#;
360        assert_eq!(json_data, expected_json);
361    }
362
363    #[test]
364    fn test_message_to_message_conversion() {
365        let user_message = message::Message::user("User message");
366        let assistant_message = message::Message::assistant("Assistant message");
367
368        let converted_user_message: Message = user_message.clone().try_into().unwrap();
369        let converted_assistant_message: Message = assistant_message.clone().try_into().unwrap();
370
371        assert_eq!(converted_user_message.role, Role::User);
372        assert_eq!(converted_user_message.content, "User message");
373
374        assert_eq!(converted_assistant_message.role, Role::Assistant);
375        assert_eq!(converted_assistant_message.content, "Assistant message");
376
377        let back_to_user_message: message::Message = converted_user_message.try_into().unwrap();
378        let back_to_assistant_message: message::Message =
379            converted_assistant_message.try_into().unwrap();
380
381        assert_eq!(user_message, back_to_user_message);
382        assert_eq!(assistant_message, back_to_assistant_message);
383    }
384}