Skip to main content

systemprompt_client/
client.rs

1use crate::error::{ClientError, ClientResult};
2use crate::http;
3use chrono::Utc;
4use reqwest::Client;
5use std::time::Duration;
6use systemprompt_identifiers::{ContextId, JwtToken};
7use systemprompt_models::a2a::{Task, methods};
8use systemprompt_models::admin::{AnalyticsData, LogEntry, UserInfo};
9use systemprompt_models::net::{
10    HTTP_AUTH_VERIFY_TIMEOUT, HTTP_DEFAULT_TIMEOUT, HTTP_HEALTH_CHECK_TIMEOUT,
11};
12use systemprompt_models::{
13    AgentCard, ApiPaths, CollectionResponse, CreateContextRequest, SingleResponse, UserContext,
14    UserContextWithStats,
15};
16
17#[derive(Debug, Clone)]
18pub struct SystempromptClient {
19    base_url: String,
20    token: Option<JwtToken>,
21    client: Client,
22}
23
24impl SystempromptClient {
25    pub fn new(base_url: &str) -> ClientResult<Self> {
26        let client = Client::builder().timeout(HTTP_DEFAULT_TIMEOUT).build()?;
27
28        Ok(Self {
29            base_url: base_url.trim_end_matches('/').to_string(),
30            token: None,
31            client,
32        })
33    }
34
35    pub fn with_timeout(base_url: &str, timeout_secs: u64) -> ClientResult<Self> {
36        let client = Client::builder()
37            .timeout(Duration::from_secs(timeout_secs))
38            .build()?;
39
40        Ok(Self {
41            base_url: base_url.trim_end_matches('/').to_string(),
42            token: None,
43            client,
44        })
45    }
46
47    #[must_use]
48    pub fn with_token(mut self, token: JwtToken) -> Self {
49        self.token = Some(token);
50        self
51    }
52
53    pub fn set_token(&mut self, token: JwtToken) {
54        self.token = Some(token);
55    }
56
57    #[must_use]
58    pub const fn token(&self) -> Option<&JwtToken> {
59        self.token.as_ref()
60    }
61
62    #[must_use]
63    pub fn base_url(&self) -> &str {
64        &self.base_url
65    }
66
67    pub async fn list_agents(&self) -> ClientResult<Vec<AgentCard>> {
68        let url = format!("{}{}", self.base_url, ApiPaths::AGENTS_REGISTRY);
69        let response: CollectionResponse<AgentCard> =
70            http::get(&self.client, &url, self.token.as_ref()).await?;
71        Ok(response.data)
72    }
73
74    pub async fn get_agent_card(&self, agent_name: &str) -> ClientResult<AgentCard> {
75        let url = format!(
76            "{}{}",
77            self.base_url,
78            ApiPaths::wellknown_agent_card_named(agent_name)
79        );
80        http::get(&self.client, &url, self.token.as_ref()).await
81    }
82
83    pub async fn list_contexts(&self) -> ClientResult<Vec<UserContextWithStats>> {
84        let url = format!(
85            "{}{}?sort=updated_at:desc",
86            self.base_url,
87            ApiPaths::CORE_CONTEXTS
88        );
89        let response: CollectionResponse<UserContextWithStats> =
90            http::get(&self.client, &url, self.token.as_ref()).await?;
91        Ok(response.data)
92    }
93
94    pub async fn get_context(&self, context_id: &ContextId) -> ClientResult<UserContext> {
95        let url = format!(
96            "{}{}/{}",
97            self.base_url,
98            ApiPaths::CORE_CONTEXTS,
99            context_id.as_ref()
100        );
101        let response: SingleResponse<UserContext> =
102            http::get(&self.client, &url, self.token.as_ref()).await?;
103        Ok(response.data)
104    }
105
106    pub async fn create_context(&self, name: Option<&str>) -> ClientResult<UserContext> {
107        let url = format!("{}{}", self.base_url, ApiPaths::CORE_CONTEXTS);
108        let request = CreateContextRequest {
109            name: name.map(String::from),
110        };
111        let response: SingleResponse<UserContext> =
112            http::post(&self.client, &url, &request, self.token.as_ref()).await?;
113        Ok(response.data)
114    }
115
116    pub async fn create_context_auto_name(&self) -> ClientResult<UserContext> {
117        let name = format!("Session {}", Utc::now().format("%Y-%m-%d %H:%M"));
118        self.create_context(Some(&name)).await
119    }
120
121    pub async fn fetch_or_create_context(&self) -> ClientResult<ContextId> {
122        let contexts = self.list_contexts().await?;
123        if let Some(ctx) = contexts.first() {
124            return Ok(ctx.context_id.clone());
125        }
126        let context = self.create_context_auto_name().await?;
127        Ok(context.context_id)
128    }
129
130    pub async fn update_context_name(&self, context_id: &str, name: &str) -> ClientResult<()> {
131        let url = format!(
132            "{}{}/{}",
133            self.base_url,
134            ApiPaths::CORE_CONTEXTS,
135            context_id
136        );
137        let body = serde_json::json!({ "name": name });
138        http::put(&self.client, &url, &body, self.token.as_ref()).await
139    }
140
141    pub async fn delete_context(&self, context_id: &str) -> ClientResult<()> {
142        let url = format!(
143            "{}{}/{}",
144            self.base_url,
145            ApiPaths::CORE_CONTEXTS,
146            context_id
147        );
148        http::delete(&self.client, &url, self.token.as_ref()).await
149    }
150
151    pub async fn list_tasks(&self, context_id: &str) -> ClientResult<Vec<Task>> {
152        let url = format!(
153            "{}{}/{}/tasks",
154            self.base_url,
155            ApiPaths::CORE_CONTEXTS,
156            context_id
157        );
158        http::get(&self.client, &url, self.token.as_ref()).await
159    }
160
161    pub async fn delete_task(&self, task_id: &str) -> ClientResult<()> {
162        let url = format!("{}{}/{}", self.base_url, ApiPaths::CORE_TASKS, task_id);
163        http::delete(&self.client, &url, self.token.as_ref()).await
164    }
165
166    pub async fn list_artifacts(&self, context_id: &str) -> ClientResult<Vec<serde_json::Value>> {
167        let url = format!(
168            "{}{}/{}/artifacts",
169            self.base_url,
170            ApiPaths::CORE_CONTEXTS,
171            context_id
172        );
173        http::get(&self.client, &url, self.token.as_ref()).await
174    }
175
176    pub async fn check_health(&self) -> bool {
177        let url = format!("{}{}", self.base_url, ApiPaths::HEALTH);
178        self.client
179            .get(&url)
180            .timeout(HTTP_HEALTH_CHECK_TIMEOUT)
181            .send()
182            .await
183            .is_ok()
184    }
185
186    pub async fn verify_token(&self) -> ClientResult<bool> {
187        let url = format!("{}{}", self.base_url, ApiPaths::AUTH_ME);
188        let auth = self.auth_header()?;
189        let response = self
190            .client
191            .get(&url)
192            .timeout(HTTP_AUTH_VERIFY_TIMEOUT)
193            .header("Authorization", auth)
194            .send()
195            .await?;
196
197        Ok(response.status().is_success())
198    }
199
200    pub async fn send_message(
201        &self,
202        agent_name: &str,
203        context_id: &ContextId,
204        message: serde_json::Value,
205    ) -> ClientResult<serde_json::Value> {
206        let url = format!("{}{}/{}/", self.base_url, ApiPaths::AGENTS_BASE, agent_name);
207        let request = serde_json::json!({
208            "jsonrpc": "2.0",
209            "method": methods::SEND_MESSAGE,
210            "params": { "message": message },
211            "id": context_id.as_ref()
212        });
213        http::post(&self.client, &url, &request, self.token.as_ref()).await
214    }
215
216    pub async fn list_logs(&self, limit: Option<u32>) -> ClientResult<Vec<LogEntry>> {
217        let url = limit.map_or_else(
218            || format!("{}{}", self.base_url, ApiPaths::ADMIN_LOGS),
219            |l| format!("{}{}?limit={}", self.base_url, ApiPaths::ADMIN_LOGS, l),
220        );
221        http::get(&self.client, &url, self.token.as_ref()).await
222    }
223
224    pub async fn list_users(&self, limit: Option<u32>) -> ClientResult<Vec<UserInfo>> {
225        let url = limit.map_or_else(
226            || format!("{}{}", self.base_url, ApiPaths::ADMIN_USERS),
227            |l| format!("{}{}?limit={}", self.base_url, ApiPaths::ADMIN_USERS, l),
228        );
229        http::get(&self.client, &url, self.token.as_ref()).await
230    }
231
232    pub async fn get_analytics(&self) -> ClientResult<AnalyticsData> {
233        let url = format!("{}{}", self.base_url, ApiPaths::ADMIN_ANALYTICS);
234        http::get(&self.client, &url, self.token.as_ref()).await
235    }
236
237    pub async fn list_all_artifacts(
238        &self,
239        limit: Option<u32>,
240    ) -> ClientResult<Vec<serde_json::Value>> {
241        let url = limit.map_or_else(
242            || format!("{}{}", self.base_url, ApiPaths::CORE_ARTIFACTS),
243            |l| format!("{}{}?limit={}", self.base_url, ApiPaths::CORE_ARTIFACTS, l),
244        );
245        http::get(&self.client, &url, self.token.as_ref()).await
246    }
247
248    fn auth_header(&self) -> ClientResult<String> {
249        self.token.as_ref().map_or_else(
250            || Err(ClientError::AuthError("No token configured".to_string())),
251            |token| Ok(format!("Bearer {}", token.as_str())),
252        )
253    }
254}