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