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