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 pub fn with_token(mut self, token: JwtToken) -> Self {
48 self.token = Some(token);
49 self
50 }
51
52 pub fn set_token(&mut self, token: JwtToken) {
53 self.token = Some(token);
54 }
55
56 pub const fn token(&self) -> Option<&JwtToken> {
57 self.token.as_ref()
58 }
59
60 pub fn base_url(&self) -> &str {
61 &self.base_url
62 }
63
64 pub async fn list_agents(&self) -> ClientResult<Vec<AgentCard>> {
65 let url = format!("{}{}", self.base_url, ApiPaths::AGENTS_REGISTRY);
66 let response: CollectionResponse<AgentCard> =
67 http::get(&self.client, &url, self.token.as_ref()).await?;
68 Ok(response.data)
69 }
70
71 pub async fn get_agent_card(&self, agent_name: &str) -> ClientResult<AgentCard> {
72 let url = format!(
73 "{}{}",
74 self.base_url,
75 ApiPaths::wellknown_agent_card_named(agent_name)
76 );
77 http::get(&self.client, &url, self.token.as_ref()).await
78 }
79
80 pub async fn list_contexts(&self) -> ClientResult<Vec<UserContextWithStats>> {
81 let url = format!(
82 "{}{}?sort=updated_at:desc",
83 self.base_url,
84 ApiPaths::CORE_CONTEXTS
85 );
86 let response: CollectionResponse<UserContextWithStats> =
87 http::get(&self.client, &url, self.token.as_ref()).await?;
88 Ok(response.data)
89 }
90
91 pub async fn get_context(&self, context_id: &ContextId) -> ClientResult<UserContext> {
92 let url = format!(
93 "{}{}/{}",
94 self.base_url,
95 ApiPaths::CORE_CONTEXTS,
96 context_id.as_ref()
97 );
98 let response: SingleResponse<UserContext> =
99 http::get(&self.client, &url, self.token.as_ref()).await?;
100 Ok(response.data)
101 }
102
103 pub async fn create_context(&self, name: Option<&str>) -> ClientResult<UserContext> {
104 let url = format!("{}{}", self.base_url, ApiPaths::CORE_CONTEXTS);
105 let request = CreateContextRequest {
106 name: name.map(String::from),
107 };
108 let response: SingleResponse<UserContext> =
109 http::post(&self.client, &url, &request, self.token.as_ref()).await?;
110 Ok(response.data)
111 }
112
113 pub async fn create_context_auto_name(&self) -> ClientResult<UserContext> {
114 let name = format!("Session {}", Utc::now().format("%Y-%m-%d %H:%M"));
115 self.create_context(Some(&name)).await
116 }
117
118 pub async fn fetch_or_create_context(&self) -> ClientResult<ContextId> {
119 let contexts = self.list_contexts().await?;
120 if let Some(ctx) = contexts.first() {
121 return Ok(ctx.context_id.clone());
122 }
123 let context = self.create_context_auto_name().await?;
124 Ok(context.context_id)
125 }
126
127 pub async fn update_context_name(&self, context_id: &str, name: &str) -> ClientResult<()> {
128 let url = format!(
129 "{}{}/{}",
130 self.base_url,
131 ApiPaths::CORE_CONTEXTS,
132 context_id
133 );
134 let body = serde_json::json!({ "name": name });
135 http::put(&self.client, &url, &body, self.token.as_ref()).await
136 }
137
138 pub async fn delete_context(&self, context_id: &str) -> ClientResult<()> {
139 let url = format!(
140 "{}{}/{}",
141 self.base_url,
142 ApiPaths::CORE_CONTEXTS,
143 context_id
144 );
145 http::delete(&self.client, &url, self.token.as_ref()).await
146 }
147
148 pub async fn list_tasks(&self, context_id: &str) -> ClientResult<Vec<Task>> {
149 let url = format!(
150 "{}{}/{}/tasks",
151 self.base_url,
152 ApiPaths::CORE_CONTEXTS,
153 context_id
154 );
155 http::get(&self.client, &url, self.token.as_ref()).await
156 }
157
158 pub async fn delete_task(&self, task_id: &str) -> ClientResult<()> {
159 let url = format!("{}{}/{}", self.base_url, ApiPaths::CORE_TASKS, task_id);
160 http::delete(&self.client, &url, self.token.as_ref()).await
161 }
162
163 pub async fn list_artifacts(&self, context_id: &str) -> ClientResult<Vec<serde_json::Value>> {
164 let url = format!(
165 "{}{}/{}/artifacts",
166 self.base_url,
167 ApiPaths::CORE_CONTEXTS,
168 context_id
169 );
170 http::get(&self.client, &url, self.token.as_ref()).await
171 }
172
173 pub async fn check_health(&self) -> bool {
174 let url = format!("{}{}", self.base_url, ApiPaths::HEALTH);
175 self.client
176 .get(&url)
177 .timeout(HTTP_HEALTH_CHECK_TIMEOUT)
178 .send()
179 .await
180 .is_ok()
181 }
182
183 pub async fn verify_token(&self) -> ClientResult<bool> {
184 let url = format!("{}{}", self.base_url, ApiPaths::AUTH_ME);
185 let auth = self.auth_header()?;
186 let response = self
187 .client
188 .get(&url)
189 .timeout(HTTP_AUTH_VERIFY_TIMEOUT)
190 .header("Authorization", auth)
191 .send()
192 .await?;
193
194 Ok(response.status().is_success())
195 }
196
197 pub async fn send_message(
198 &self,
199 agent_name: &str,
200 context_id: &ContextId,
201 message: serde_json::Value,
202 ) -> ClientResult<serde_json::Value> {
203 let url = format!("{}{}/{}/", self.base_url, ApiPaths::AGENTS_BASE, agent_name);
204 let request = serde_json::json!({
205 "jsonrpc": "2.0",
206 "method": methods::SEND_MESSAGE,
207 "params": { "message": message },
208 "id": context_id.as_ref()
209 });
210 http::post(&self.client, &url, &request, self.token.as_ref()).await
211 }
212
213 pub async fn list_logs(&self, limit: Option<u32>) -> ClientResult<Vec<LogEntry>> {
214 let url = limit.map_or_else(
215 || format!("{}{}", self.base_url, ApiPaths::ADMIN_LOGS),
216 |l| format!("{}{}?limit={}", self.base_url, ApiPaths::ADMIN_LOGS, l),
217 );
218 http::get(&self.client, &url, self.token.as_ref()).await
219 }
220
221 pub async fn list_users(&self, limit: Option<u32>) -> ClientResult<Vec<UserInfo>> {
222 let url = limit.map_or_else(
223 || format!("{}{}", self.base_url, ApiPaths::ADMIN_USERS),
224 |l| format!("{}{}?limit={}", self.base_url, ApiPaths::ADMIN_USERS, l),
225 );
226 http::get(&self.client, &url, self.token.as_ref()).await
227 }
228
229 pub async fn get_analytics(&self) -> ClientResult<AnalyticsData> {
230 let url = format!("{}{}", self.base_url, ApiPaths::ADMIN_ANALYTICS);
231 http::get(&self.client, &url, self.token.as_ref()).await
232 }
233
234 pub async fn list_all_artifacts(
235 &self,
236 limit: Option<u32>,
237 ) -> ClientResult<Vec<serde_json::Value>> {
238 let url = limit.map_or_else(
239 || format!("{}{}", self.base_url, ApiPaths::CORE_ARTIFACTS),
240 |l| format!("{}{}?limit={}", self.base_url, ApiPaths::CORE_ARTIFACTS, l),
241 );
242 http::get(&self.client, &url, self.token.as_ref()).await
243 }
244
245 fn auth_header(&self) -> ClientResult<String> {
246 self.token.as_ref().map_or_else(
247 || Err(ClientError::AuthError("No token configured".to_string())),
248 |token| Ok(format!("Bearer {}", token.as_str())),
249 )
250 }
251}