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