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