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