chasm_cli/providers/cloud/
perplexity.rs1use super::common::{
14 build_http_client, CloudConversation, CloudMessage, CloudProvider, FetchOptions,
15 HttpClientConfig,
16};
17use anyhow::{anyhow, Result};
18use chrono::{DateTime, Utc};
19use serde::Deserialize;
20
21const PERPLEXITY_API: &str = "https://api.perplexity.ai";
22const PERPLEXITY_WEB_API: &str = "https://www.perplexity.ai/api";
23
24pub struct PerplexityProvider {
26 api_key: Option<String>,
27 session_token: Option<String>,
28 client: Option<reqwest::blocking::Client>,
29}
30
31impl PerplexityProvider {
32 pub fn new(api_key: Option<String>) -> Self {
33 Self {
34 api_key,
35 session_token: None,
36 client: None,
37 }
38 }
39
40 fn ensure_client(&mut self) -> Result<&reqwest::blocking::Client> {
41 if self.client.is_none() {
42 let config = HttpClientConfig::default();
43 self.client = Some(build_http_client(&config)?);
44 }
45 Ok(self.client.as_ref().unwrap())
46 }
47}
48
49#[derive(Debug, Deserialize)]
50struct PerplexityThread {
51 uuid: String,
52 #[serde(default)]
53 title: Option<String>,
54 created_at: String,
55 updated_at: String,
56 #[serde(default)]
57 messages: Vec<PerplexityMessage>,
58}
59
60#[derive(Debug, Deserialize)]
61struct PerplexityMessage {
62 uuid: String,
63 text: String,
64 role: String, created_at: String,
66 #[serde(default)]
67 sources: Vec<PerplexitySource>,
68}
69
70#[derive(Debug, Deserialize)]
71struct PerplexitySource {
72 url: String,
73 title: Option<String>,
74}
75
76impl CloudProvider for PerplexityProvider {
77 fn name(&self) -> &'static str {
78 "Perplexity"
79 }
80
81 fn api_base_url(&self) -> &str {
82 PERPLEXITY_WEB_API
83 }
84
85 fn is_authenticated(&self) -> bool {
86 self.api_key.is_some() || self.session_token.is_some()
87 }
88
89 fn set_credentials(&mut self, api_key: Option<String>, session_token: Option<String>) {
90 self.api_key = api_key;
91 self.session_token = session_token;
92 }
93
94 fn list_conversations(&self, _options: &FetchOptions) -> Result<Vec<CloudConversation>> {
95 if !self.is_authenticated() {
96 return Err(anyhow!(
97 "Perplexity requires authentication. Set PERPLEXITY_API_KEY or provide a session token."
98 ));
99 }
100
101 eprintln!("Note: Perplexity conversation history requires web session authentication.");
102 eprintln!("The Perplexity API is stateless and doesn't store conversation history.");
103
104 Ok(vec![])
105 }
106
107 fn fetch_conversation(&self, _id: &str) -> Result<CloudConversation> {
108 if !self.is_authenticated() {
109 return Err(anyhow!("Perplexity requires authentication"));
110 }
111
112 Err(anyhow!(
113 "Fetching Perplexity conversations requires web session authentication."
114 ))
115 }
116
117 fn api_key_env_var(&self) -> &'static str {
118 "PERPLEXITY_API_KEY"
119 }
120}
121
122pub fn parse_perplexity_export(json_data: &str) -> Result<Vec<CloudConversation>> {
124 let threads: Vec<PerplexityThread> = serde_json::from_str(json_data)?;
125
126 Ok(threads
127 .into_iter()
128 .map(|thread| {
129 CloudConversation {
130 id: thread.uuid,
131 title: thread.title,
132 created_at: parse_iso_timestamp(&thread.created_at).unwrap_or_else(|_| Utc::now()),
133 updated_at: Some(
134 parse_iso_timestamp(&thread.updated_at).unwrap_or_else(|_| Utc::now()),
135 ),
136 model: Some("perplexity".to_string()),
137 messages: thread
138 .messages
139 .into_iter()
140 .map(|msg| {
141 let content = if !msg.sources.is_empty() && msg.role == "assistant" {
143 let sources_text = msg
144 .sources
145 .iter()
146 .enumerate()
147 .map(|(i, s)| format!("[{}] {}", i + 1, s.url))
148 .collect::<Vec<_>>()
149 .join("\n");
150 format!("{}\n\nSources:\n{}", msg.text, sources_text)
151 } else {
152 msg.text
153 };
154
155 CloudMessage {
156 id: Some(msg.uuid),
157 role: msg.role,
158 content,
159 timestamp: parse_iso_timestamp(&msg.created_at).ok(),
160 model: None,
161 }
162 })
163 .collect(),
164 metadata: None,
165 }
166 })
167 .collect())
168}
169
170fn parse_iso_timestamp(s: &str) -> Result<DateTime<Utc>> {
171 DateTime::parse_from_rfc3339(s)
172 .map(|dt| dt.with_timezone(&Utc))
173 .map_err(|e| anyhow!("Failed to parse timestamp: {}", e))
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_perplexity_provider_new() {
182 let provider = PerplexityProvider::new(Some("test-key".to_string()));
183 assert_eq!(provider.name(), "Perplexity");
184 assert!(provider.is_authenticated());
185 }
186
187 #[test]
188 fn test_perplexity_provider_unauthenticated() {
189 let provider = PerplexityProvider::new(None);
190 assert!(!provider.is_authenticated());
191 }
192
193 #[test]
194 fn test_api_key_env_var() {
195 let provider = PerplexityProvider::new(None);
196 assert_eq!(provider.api_key_env_var(), "PERPLEXITY_API_KEY");
197 }
198}