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