chasm_cli/providers/cloud/
common.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: Apache-2.0
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                };
92
93                // Check if next message is assistant response
94                if i + 1 < self.messages.len() && self.messages[i + 1].role == "assistant" {
95                    let assistant_msg = &self.messages[i + 1];
96                    request.response = Some(serde_json::json!({
97                        "text": assistant_msg.content,
98                        "result": {
99                            "metadata": {
100                                "provider": provider_name,
101                                "model": assistant_msg.model.clone().or_else(|| self.model.clone())
102                            }
103                        }
104                    }));
105                    request.response_id = Some(
106                        assistant_msg
107                            .id
108                            .clone()
109                            .unwrap_or_else(|| Uuid::new_v4().to_string()),
110                    );
111                    i += 1;
112                }
113
114                requests.push(request);
115            } else if msg.role == "system" {
116                // Skip system messages or add as metadata
117                // Could be added to session metadata if needed
118            }
119            i += 1;
120        }
121
122        ChatSession {
123            version: 3,
124            session_id: Some(format!("{}:{}", provider_name, self.id)),
125            creation_date: self.created_at.timestamp_millis(),
126            last_message_date: self
127                .updated_at
128                .unwrap_or(self.created_at)
129                .timestamp_millis(),
130            is_imported: true,
131            initial_location: "panel".to_string(),
132            custom_title: self.title.clone(),
133            requester_username: None,
134            requester_avatar_icon_uri: None,
135            responder_username: Some(provider_name.to_string()),
136            responder_avatar_icon_uri: None,
137            requests,
138        }
139    }
140}
141
142/// Trait for cloud provider implementations
143pub trait CloudProvider: Send + Sync {
144    /// Get the provider name
145    fn name(&self) -> &'static str;
146
147    /// Get the API base URL
148    fn api_base_url(&self) -> &str;
149
150    /// Check if the provider is authenticated
151    fn is_authenticated(&self) -> bool;
152
153    /// Set the API key or session token
154    fn set_credentials(&mut self, api_key: Option<String>, session_token: Option<String>);
155
156    /// List available conversations
157    fn list_conversations(&self, options: &FetchOptions) -> Result<Vec<CloudConversation>>;
158
159    /// Fetch a single conversation by ID
160    fn fetch_conversation(&self, id: &str) -> Result<CloudConversation>;
161
162    /// Fetch all conversations (with messages)
163    fn fetch_all_conversations(&self, options: &FetchOptions) -> Result<Vec<ChatSession>> {
164        let conversations = self.list_conversations(options)?;
165        let mut sessions = Vec::new();
166
167        for conv in conversations {
168            // If messages are already populated, use them directly
169            if !conv.messages.is_empty() {
170                sessions.push(conv.to_chat_session(self.name()));
171            } else {
172                // Otherwise fetch the full conversation
173                match self.fetch_conversation(&conv.id) {
174                    Ok(full_conv) => sessions.push(full_conv.to_chat_session(self.name())),
175                    Err(e) => {
176                        eprintln!("Warning: Failed to fetch conversation {}: {}", conv.id, e);
177                    }
178                }
179            }
180        }
181
182        Ok(sessions)
183    }
184
185    /// Get the environment variable name for the API key
186    fn api_key_env_var(&self) -> &'static str;
187
188    /// Attempt to load API key from environment
189    fn load_api_key_from_env(&self) -> Option<String> {
190        std::env::var(self.api_key_env_var()).ok()
191    }
192}
193
194/// HTTP client configuration for cloud providers
195#[derive(Debug, Clone)]
196pub struct HttpClientConfig {
197    pub timeout_secs: u64,
198    pub user_agent: String,
199    pub accept_invalid_certs: bool,
200}
201
202impl Default for HttpClientConfig {
203    fn default() -> Self {
204        Self {
205            timeout_secs: 30,
206            user_agent: format!("csm/{}", env!("CARGO_PKG_VERSION")),
207            accept_invalid_certs: false,
208        }
209    }
210}
211
212/// Build a configured HTTP client
213pub fn build_http_client(config: &HttpClientConfig) -> Result<reqwest::blocking::Client> {
214    use std::time::Duration;
215
216    reqwest::blocking::Client::builder()
217        .timeout(Duration::from_secs(config.timeout_secs))
218        .user_agent(&config.user_agent)
219        .danger_accept_invalid_certs(config.accept_invalid_certs)
220        .build()
221        .map_err(|e| anyhow::anyhow!("Failed to build HTTP client: {}", e))
222}