Skip to main content

chasm/providers/cloud/
common.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! Common types and traits for cloud providers
4
5use crate::models::{ChatMessage, ChatRequest, ChatSession};
6use anyhow::Result;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// Options for fetching conversations from cloud providers
11#[derive(Debug, Clone, Default)]
12pub struct FetchOptions {
13    /// Maximum number of conversations to fetch
14    pub limit: Option<usize>,
15    /// Fetch only conversations after this date
16    pub after: Option<DateTime<Utc>>,
17    /// Fetch only conversations before this date
18    pub before: Option<DateTime<Utc>>,
19    /// Include archived conversations
20    pub include_archived: bool,
21    /// Session token for web API authentication (alternative to API key)
22    pub session_token: Option<String>,
23}
24
25/// Represents a conversation from a cloud provider
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CloudConversation {
28    /// Unique identifier from the cloud provider
29    pub id: String,
30    /// Title of the conversation
31    pub title: Option<String>,
32    /// When the conversation was created
33    pub created_at: DateTime<Utc>,
34    /// When the conversation was last updated
35    pub updated_at: Option<DateTime<Utc>>,
36    /// Model used in the conversation
37    pub model: Option<String>,
38    /// Messages in the conversation
39    pub messages: Vec<CloudMessage>,
40    /// Additional metadata
41    pub metadata: Option<serde_json::Value>,
42}
43
44/// Represents a message in a cloud conversation
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct CloudMessage {
47    /// Unique identifier
48    pub id: Option<String>,
49    /// Role: user, assistant, system
50    pub role: String,
51    /// Message content
52    pub content: String,
53    /// Timestamp
54    pub timestamp: Option<DateTime<Utc>>,
55    /// Model that generated this message (for assistant messages)
56    pub model: Option<String>,
57}
58
59impl CloudConversation {
60    /// Convert to a ChatSession for import (VS Code format)
61    pub fn to_chat_session(&self, provider_name: &str) -> ChatSession {
62        use uuid::Uuid;
63
64        // Generate requests from message pairs
65        let mut requests = Vec::new();
66        let mut i = 0;
67        while i < self.messages.len() {
68            let msg = &self.messages[i];
69
70            if msg.role == "user" {
71                // Create a request with the user message
72                let mut request = ChatRequest {
73                    timestamp: msg.timestamp.map(|t| t.timestamp_millis()),
74                    message: Some(ChatMessage {
75                        text: Some(msg.content.clone()),
76                        parts: None,
77                    }),
78                    response: None,
79                    variable_data: None,
80                    request_id: Some(msg.id.clone().unwrap_or_else(|| Uuid::new_v4().to_string())),
81                    response_id: None,
82                    model_id: self.model.clone(),
83                    agent: None,
84                    result: None,
85                    followups: None,
86                    is_canceled: Some(false),
87                    content_references: None,
88                    code_citations: None,
89                    response_markdown_info: None,
90                    source_session: Some(format!("{}:{}", provider_name, self.id)),
91                    model_state: None,
92                    time_spent_waiting: None,
93                };
94
95                // Check if next message is assistant response
96                if i + 1 < self.messages.len() && self.messages[i + 1].role == "assistant" {
97                    let assistant_msg = &self.messages[i + 1];
98                    request.response = Some(serde_json::json!({
99                        "text": assistant_msg.content,
100                        "result": {
101                            "metadata": {
102                                "provider": provider_name,
103                                "model": assistant_msg.model.clone().or_else(|| self.model.clone())
104                            }
105                        }
106                    }));
107                    request.response_id = Some(
108                        assistant_msg
109                            .id
110                            .clone()
111                            .unwrap_or_else(|| Uuid::new_v4().to_string()),
112                    );
113                    i += 1;
114                }
115
116                requests.push(request);
117            } else if msg.role == "system" {
118                // Skip system messages or add as metadata
119                // Could be added to session metadata if needed
120            }
121            i += 1;
122        }
123
124        ChatSession {
125            version: 3,
126            session_id: Some(format!("{}:{}", provider_name, self.id)),
127            creation_date: self.created_at.timestamp_millis(),
128            last_message_date: self
129                .updated_at
130                .unwrap_or(self.created_at)
131                .timestamp_millis(),
132            is_imported: true,
133            initial_location: "panel".to_string(),
134            custom_title: self.title.clone(),
135            requester_username: None,
136            requester_avatar_icon_uri: None,
137            responder_username: Some(provider_name.to_string()),
138            responder_avatar_icon_uri: None,
139            requests,
140        }
141    }
142}
143
144/// Trait for cloud provider implementations
145pub trait CloudProvider: Send + Sync {
146    /// Get the provider name
147    fn name(&self) -> &'static str;
148
149    /// Get the API base URL
150    fn api_base_url(&self) -> &str;
151
152    /// Check if the provider is authenticated
153    fn is_authenticated(&self) -> bool;
154
155    /// Set the API key or session token
156    fn set_credentials(&mut self, api_key: Option<String>, session_token: Option<String>);
157
158    /// List available conversations
159    fn list_conversations(&self, options: &FetchOptions) -> Result<Vec<CloudConversation>>;
160
161    /// Fetch a single conversation by ID
162    fn fetch_conversation(&self, id: &str) -> Result<CloudConversation>;
163
164    /// Fetch all conversations (with messages)
165    fn fetch_all_conversations(&self, options: &FetchOptions) -> Result<Vec<ChatSession>> {
166        let conversations = self.list_conversations(options)?;
167        let mut sessions = Vec::new();
168
169        for conv in conversations {
170            // If messages are already populated, use them directly
171            if !conv.messages.is_empty() {
172                sessions.push(conv.to_chat_session(self.name()));
173            } else {
174                // Otherwise fetch the full conversation
175                match self.fetch_conversation(&conv.id) {
176                    Ok(full_conv) => sessions.push(full_conv.to_chat_session(self.name())),
177                    Err(e) => {
178                        eprintln!("Warning: Failed to fetch conversation {}: {}", conv.id, e);
179                    }
180                }
181            }
182        }
183
184        Ok(sessions)
185    }
186
187    /// Get the environment variable name for the API key
188    fn api_key_env_var(&self) -> &'static str;
189
190    /// Attempt to load API key from environment
191    fn load_api_key_from_env(&self) -> Option<String> {
192        std::env::var(self.api_key_env_var()).ok()
193    }
194}
195
196/// HTTP client configuration for cloud providers
197#[derive(Debug, Clone)]
198pub struct HttpClientConfig {
199    pub timeout_secs: u64,
200    pub user_agent: String,
201    pub accept_invalid_certs: bool,
202}
203
204impl Default for HttpClientConfig {
205    fn default() -> Self {
206        Self {
207            timeout_secs: 30,
208            user_agent: format!("csm/{}", env!("CARGO_PKG_VERSION")),
209            accept_invalid_certs: false,
210        }
211    }
212}
213
214/// Build a configured HTTP client
215pub fn build_http_client(config: &HttpClientConfig) -> Result<reqwest::blocking::Client> {
216    use std::time::Duration;
217
218    reqwest::blocking::Client::builder()
219        .timeout(Duration::from_secs(config.timeout_secs))
220        .user_agent(&config.user_agent)
221        .danger_accept_invalid_certs(config.accept_invalid_certs)
222        .build()
223        .map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e))
224}