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(
179 &self,
180 context_id: &ContextId,
181 ) -> ClientResult<Vec<serde_json::Value>> {
182 let url = format!(
183 "{}{}/{}/artifacts",
184 self.base_url,
185 ApiPaths::CORE_CONTEXTS,
186 context_id.as_str()
187 );
188 http::get(&self.client, &url, self.token.as_ref()).await
189 }
190
191 pub async fn check_health(&self) -> bool {
192 let url = format!("{}{}", self.base_url, ApiPaths::HEALTH);
193 self.client
194 .get(&url)
195 .timeout(HTTP_HEALTH_CHECK_TIMEOUT)
196 .send()
197 .await
198 .is_ok()
199 }
200
201 pub async fn verify_token(&self) -> ClientResult<bool> {
202 let url = format!("{}{}", self.base_url, ApiPaths::AUTH_ME);
203 let auth = self.auth_header()?;
204 let response = self
205 .client
206 .get(&url)
207 .timeout(HTTP_AUTH_VERIFY_TIMEOUT)
208 .header("Authorization", auth)
209 .send()
210 .await?;
211
212 Ok(response.status().is_success())
213 }
214
215 pub async fn send_message(
219 &self,
220 agent_name: &str,
221 context_id: &ContextId,
222 message: serde_json::Value,
223 ) -> ClientResult<serde_json::Value> {
224 let url = format!("{}{}/{}/", self.base_url, ApiPaths::AGENTS_BASE, agent_name);
225 let request = serde_json::json!({
226 "jsonrpc": "2.0",
227 "method": methods::SEND_MESSAGE,
228 "params": { "message": message },
229 "id": context_id.as_ref()
230 });
231 http::post(&self.client, &url, &request, self.token.as_ref()).await
232 }
233
234 fn limited_url(&self, path: &str, limit: Option<u32>) -> String {
235 limit.map_or_else(
236 || format!("{}{}", self.base_url, path),
237 |l| format!("{}{}?limit={}", self.base_url, path, l),
238 )
239 }
240
241 pub async fn list_logs(&self, limit: Option<u32>) -> ClientResult<Vec<LogEntry>> {
242 let url = self.limited_url(ApiPaths::ADMIN_LOGS, limit);
243 http::get(&self.client, &url, self.token.as_ref()).await
244 }
245
246 pub async fn list_users(&self, limit: Option<u32>) -> ClientResult<Vec<UserInfo>> {
247 let url = self.limited_url(ApiPaths::ADMIN_USERS, limit);
248 http::get(&self.client, &url, self.token.as_ref()).await
249 }
250
251 pub async fn get_analytics(&self) -> ClientResult<AnalyticsData> {
252 let url = format!("{}{}", self.base_url, ApiPaths::ADMIN_ANALYTICS);
253 http::get(&self.client, &url, self.token.as_ref()).await
254 }
255
256 pub async fn list_all_artifacts(
258 &self,
259 limit: Option<u32>,
260 ) -> ClientResult<Vec<serde_json::Value>> {
261 let url = self.limited_url(ApiPaths::CORE_ARTIFACTS, limit);
262 http::get(&self.client, &url, self.token.as_ref()).await
263 }
264
265 fn auth_header(&self) -> ClientResult<String> {
266 self.token.as_ref().map_or_else(
267 || Err(ClientError::AuthError("No token configured".to_string())),
268 |token| Ok(format!("Bearer {}", token.as_str())),
269 )
270 }
271}