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