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, 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(&self, context_id: &str, name: &str) -> ClientResult<()> {
131 let url = format!(
132 "{}{}/{}",
133 self.base_url,
134 ApiPaths::CORE_CONTEXTS,
135 context_id
136 );
137 let body = serde_json::json!({ "name": name });
138 http::put(&self.client, &url, &body, self.token.as_ref()).await
139 }
140
141 pub async fn delete_context(&self, context_id: &str) -> ClientResult<()> {
142 let url = format!(
143 "{}{}/{}",
144 self.base_url,
145 ApiPaths::CORE_CONTEXTS,
146 context_id
147 );
148 http::delete(&self.client, &url, self.token.as_ref()).await
149 }
150
151 pub async fn list_tasks(&self, context_id: &str) -> ClientResult<Vec<Task>> {
152 let url = format!(
153 "{}{}/{}/tasks",
154 self.base_url,
155 ApiPaths::CORE_CONTEXTS,
156 context_id
157 );
158 http::get(&self.client, &url, self.token.as_ref()).await
159 }
160
161 pub async fn delete_task(&self, task_id: &str) -> ClientResult<()> {
162 let url = format!("{}{}/{}", self.base_url, ApiPaths::CORE_TASKS, task_id);
163 http::delete(&self.client, &url, self.token.as_ref()).await
164 }
165
166 pub async fn list_artifacts(&self, context_id: &str) -> ClientResult<Vec<serde_json::Value>> {
167 let url = format!(
168 "{}{}/{}/artifacts",
169 self.base_url,
170 ApiPaths::CORE_CONTEXTS,
171 context_id
172 );
173 http::get(&self.client, &url, self.token.as_ref()).await
174 }
175
176 pub async fn check_health(&self) -> bool {
177 let url = format!("{}{}", self.base_url, ApiPaths::HEALTH);
178 self.client
179 .get(&url)
180 .timeout(HTTP_HEALTH_CHECK_TIMEOUT)
181 .send()
182 .await
183 .is_ok()
184 }
185
186 pub async fn verify_token(&self) -> ClientResult<bool> {
187 let url = format!("{}{}", self.base_url, ApiPaths::AUTH_ME);
188 let auth = self.auth_header()?;
189 let response = self
190 .client
191 .get(&url)
192 .timeout(HTTP_AUTH_VERIFY_TIMEOUT)
193 .header("Authorization", auth)
194 .send()
195 .await?;
196
197 Ok(response.status().is_success())
198 }
199
200 pub async fn send_message(
201 &self,
202 agent_name: &str,
203 context_id: &ContextId,
204 message: serde_json::Value,
205 ) -> ClientResult<serde_json::Value> {
206 let url = format!("{}{}/{}/", self.base_url, ApiPaths::AGENTS_BASE, agent_name);
207 let request = serde_json::json!({
208 "jsonrpc": "2.0",
209 "method": methods::SEND_MESSAGE,
210 "params": { "message": message },
211 "id": context_id.as_ref()
212 });
213 http::post(&self.client, &url, &request, self.token.as_ref()).await
214 }
215
216 pub async fn list_logs(&self, limit: Option<u32>) -> ClientResult<Vec<LogEntry>> {
217 let url = limit.map_or_else(
218 || format!("{}{}", self.base_url, ApiPaths::ADMIN_LOGS),
219 |l| format!("{}{}?limit={}", self.base_url, ApiPaths::ADMIN_LOGS, l),
220 );
221 http::get(&self.client, &url, self.token.as_ref()).await
222 }
223
224 pub async fn list_users(&self, limit: Option<u32>) -> ClientResult<Vec<UserInfo>> {
225 let url = limit.map_or_else(
226 || format!("{}{}", self.base_url, ApiPaths::ADMIN_USERS),
227 |l| format!("{}{}?limit={}", self.base_url, ApiPaths::ADMIN_USERS, l),
228 );
229 http::get(&self.client, &url, self.token.as_ref()).await
230 }
231
232 pub async fn get_analytics(&self) -> ClientResult<AnalyticsData> {
233 let url = format!("{}{}", self.base_url, ApiPaths::ADMIN_ANALYTICS);
234 http::get(&self.client, &url, self.token.as_ref()).await
235 }
236
237 pub async fn list_all_artifacts(
238 &self,
239 limit: Option<u32>,
240 ) -> ClientResult<Vec<serde_json::Value>> {
241 let url = limit.map_or_else(
242 || format!("{}{}", self.base_url, ApiPaths::CORE_ARTIFACTS),
243 |l| format!("{}{}?limit={}", self.base_url, ApiPaths::CORE_ARTIFACTS, l),
244 );
245 http::get(&self.client, &url, self.token.as_ref()).await
246 }
247
248 fn auth_header(&self) -> ClientResult<String> {
249 self.token.as_ref().map_or_else(
250 || Err(ClientError::AuthError("No token configured".to_string())),
251 |token| Ok(format!("Bearer {}", token.as_str())),
252 )
253 }
254}