1use 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
23const 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 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
98pub const SONAR_PRO: &str = "sonar-pro";
103pub 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 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 let prompt_with_context = completion_request.prompt_with_context();
270
271 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 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 messages.push(
292 prompt_with_context
293 .try_into()
294 .map_err(|e: MessageError| CompletionError::RequestError(e.into()))?,
295 );
296
297 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}