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, TaskId};
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(
131        &self,
132        context_id: &ContextId,
133        name: &str,
134    ) -> ClientResult<()> {
135        let url = format!(
136            "{}{}/{}",
137            self.base_url,
138            ApiPaths::CORE_CONTEXTS,
139            context_id.as_str()
140        );
141        let body = serde_json::json!({ "name": name });
142        http::put(&self.client, &url, &body, self.token.as_ref()).await
143    }
144
145    pub async fn delete_context(&self, context_id: &ContextId) -> ClientResult<()> {
146        let url = format!(
147            "{}{}/{}",
148            self.base_url,
149            ApiPaths::CORE_CONTEXTS,
150            context_id.as_str()
151        );
152        http::delete(&self.client, &url, self.token.as_ref()).await
153    }
154
155    pub async fn list_tasks(&self, context_id: &ContextId) -> ClientResult<Vec<Task>> {
156        let url = format!(
157            "{}{}/{}/tasks",
158            self.base_url,
159            ApiPaths::CORE_CONTEXTS,
160            context_id.as_str()
161        );
162        http::get(&self.client, &url, self.token.as_ref()).await
163    }
164
165    pub async fn delete_task(&self, task_id: &TaskId) -> ClientResult<()> {
166        let url = format!(
167            "{}{}/{}",
168            self.base_url,
169            ApiPaths::CORE_TASKS,
170            task_id.as_str()
171        );
172        http::delete(&self.client, &url, self.token.as_ref()).await
173    }
174
175    pub async fn list_artifacts(
176        &self,
177        context_id: &ContextId,
178    ) -> ClientResult<Vec<serde_json::Value>> {
179        let url = format!(
180            "{}{}/{}/artifacts",
181            self.base_url,
182            ApiPaths::CORE_CONTEXTS,
183            context_id.as_str()
184        );
185        http::get(&self.client, &url, self.token.as_ref()).await
186    }
187
188    pub async fn check_health(&self) -> bool {
189        let url = format!("{}{}", self.base_url, ApiPaths::HEALTH);
190        self.client
191            .get(&url)
192            .timeout(HTTP_HEALTH_CHECK_TIMEOUT)
193            .send()
194            .await
195            .is_ok()
196    }
197
198    pub async fn verify_token(&self) -> ClientResult<bool> {
199        let url = format!("{}{}", self.base_url, ApiPaths::AUTH_ME);
200        let auth = self.auth_header()?;
201        let response = self
202            .client
203            .get(&url)
204            .timeout(HTTP_AUTH_VERIFY_TIMEOUT)
205            .header("Authorization", auth)
206            .send()
207            .await?;
208
209        Ok(response.status().is_success())
210    }
211
212    pub async fn send_message(
213        &self,
214        agent_name: &str,
215        context_id: &ContextId,
216        message: serde_json::Value,
217    ) -> ClientResult<serde_json::Value> {
218        let url = format!("{}{}/{}/", self.base_url, ApiPaths::AGENTS_BASE, agent_name);
219        let request = serde_json::json!({
220            "jsonrpc": "2.0",
221            "method": methods::SEND_MESSAGE,
222            "params": { "message": message },
223            "id": context_id.as_ref()
224        });
225        http::post(&self.client, &url, &request, self.token.as_ref()).await
226    }
227
228    fn limited_url(&self, path: &str, limit: Option<u32>) -> String {
229        limit.map_or_else(
230            || format!("{}{}", self.base_url, path),
231            |l| format!("{}{}?limit={}", self.base_url, path, l),
232        )
233    }
234
235    pub async fn list_logs(&self, limit: Option<u32>) -> ClientResult<Vec<LogEntry>> {
236        let url = self.limited_url(ApiPaths::ADMIN_LOGS, limit);
237        http::get(&self.client, &url, self.token.as_ref()).await
238    }
239
240    pub async fn list_users(&self, limit: Option<u32>) -> ClientResult<Vec<UserInfo>> {
241        let url = self.limited_url(ApiPaths::ADMIN_USERS, limit);
242        http::get(&self.client, &url, self.token.as_ref()).await
243    }
244
245    pub async fn get_analytics(&self) -> ClientResult<AnalyticsData> {
246        let url = format!("{}{}", self.base_url, ApiPaths::ADMIN_ANALYTICS);
247        http::get(&self.client, &url, self.token.as_ref()).await
248    }
249
250    pub async fn list_all_artifacts(
251        &self,
252        limit: Option<u32>,
253    ) -> ClientResult<Vec<serde_json::Value>> {
254        let url = self.limited_url(ApiPaths::CORE_ARTIFACTS, limit);
255        http::get(&self.client, &url, self.token.as_ref()).await
256    }
257
258    fn auth_header(&self) -> ClientResult<String> {
259        self.token.as_ref().map_or_else(
260            || Err(ClientError::AuthError("No token configured".to_string())),
261            |token| Ok(format!("Bearer {}", token.as_str())),
262        )
263    }
264}