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