1use crate::config::{AxonFlowConfig, Mode};
2use crate::error::AxonFlowError;
3use crate::heartbeat::maybe_send_heartbeat;
4use crate::types::agent::{ClientRequest, ClientResponse};
5use crate::PATH_SEGMENT;
6use base64::engine::general_purpose::STANDARD as BASE64_STD;
7use base64::Engine as _;
8use moka::future::Cache;
9use percent_encoding::utf8_percent_encode;
10use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
11use std::collections::HashMap;
12use std::sync::Arc;
13use std::time::Duration;
14use tracing::{debug, warn};
15
16const LICENSE_KEY_HEADER: &str = "X-License-Key";
17
18#[derive(Clone)]
19pub struct AxonFlowClient {
20 config: AxonFlowConfig,
21 http_client: reqwest::Client,
22 map_http_client: reqwest::Client,
23 cache: Option<Arc<Cache<String, ClientResponse>>>,
24}
25
26impl AxonFlowClient {
27 pub fn new(mut config: AxonFlowConfig) -> Result<Self, AxonFlowError> {
28 if config.retry.max_attempts == 0 {
29 return Err(AxonFlowError::ConfigError(
30 "retry.max_attempts must be at least 1".to_string(),
31 ));
32 }
33
34 if std::env::var("AXONFLOW_TRY").unwrap_or_default() == "1" {
35 config.endpoint = "https://try.getaxonflow.com".to_string();
36 if config.client_id.is_none() {
37 return Err(AxonFlowError::ConfigError(
38 "ClientID is required in try mode (AXONFLOW_TRY=1).".to_string(),
39 ));
40 }
41 }
42
43 if config.client_secret.is_some() && config.client_id.is_none() {
44 warn!("ClientID is required when ClientSecret is set.");
45 }
46
47 let mut headers = HeaderMap::new();
48 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
49 headers.insert(
50 "User-Agent",
51 HeaderValue::from_static(concat!("axonflow-sdk-rust/", env!("CARGO_PKG_VERSION"))),
52 );
53 headers.insert(
59 "X-Axonflow-Client",
60 HeaderValue::from_static(concat!("sdk-rust/", env!("CARGO_PKG_VERSION"))),
61 );
62
63 let basic_id = config.client_id.as_deref().unwrap_or("community");
67 let basic_secret = config.client_secret.as_deref().unwrap_or("");
68 let basic_credentials = BASE64_STD.encode(format!("{basic_id}:{basic_secret}"));
69 let basic_value = format!("Basic {}", basic_credentials);
70 if let Ok(val) = HeaderValue::from_str(&basic_value) {
71 headers.insert(AUTHORIZATION, val);
72 }
73
74 if let Ok(val) = HeaderValue::from_str(basic_id) {
79 headers.insert("X-Client-ID", val);
80 }
81
82 if let Some(license_key) = &config.license_key {
84 if let Ok(mut val) = HeaderValue::from_str(license_key) {
85 val.set_sensitive(true);
86 headers.insert(LICENSE_KEY_HEADER, val);
87 }
88 }
89
90 let accept_invalid = config.insecure_skip_tls_verify
91 || std::env::var("AXONFLOW_INSECURE_TLS").unwrap_or_default() == "1";
92
93 if accept_invalid {
94 warn!("TLS certificate verification is disabled.");
95 }
96
97 let http_client = reqwest::Client::builder()
98 .timeout(config.timeout)
99 .default_headers(headers.clone())
100 .danger_accept_invalid_certs(accept_invalid)
101 .pool_max_idle_per_host(5)
102 .tcp_keepalive(Duration::from_secs(30))
103 .build()
104 .map_err(AxonFlowError::HttpError)?;
105
106 let map_http_client = reqwest::Client::builder()
107 .timeout(config.map_timeout)
108 .default_headers(headers)
109 .danger_accept_invalid_certs(accept_invalid)
110 .pool_max_idle_per_host(5)
111 .tcp_keepalive(Duration::from_secs(30))
112 .build()
113 .map_err(AxonFlowError::HttpError)?;
114
115 let cache = if config.cache.enabled {
116 Some(Arc::new(
117 Cache::builder()
118 .time_to_live(config.cache.ttl)
119 .max_capacity(config.cache.max_capacity)
120 .build(),
121 ))
122 } else {
123 None
124 };
125
126 maybe_send_heartbeat(&config.endpoint, &config.mode);
127
128 Ok(Self {
129 config,
130 http_client,
131 map_http_client,
132 cache,
133 })
134 }
135
136 #[tracing::instrument(skip(self, context))]
137 pub async fn proxy_llm_call(
138 &self,
139 user_token: &str,
140 query: &str,
141 request_type: &str,
142 context: HashMap<String, serde_json::Value>,
143 ) -> Result<ClientResponse, AxonFlowError> {
144 let user_token = if user_token.is_empty() {
145 "anonymous"
146 } else {
147 user_token
148 };
149
150 let is_mutation = matches!(
151 request_type,
152 "execute-plan" | "generate-plan" | "cancel-plan" | "update-plan"
153 );
154
155 if !is_mutation {
156 if let Some(cache) = &self.cache {
157 let cache_key = self.build_cache_key(request_type, query, user_token, &context);
158 if let Some(cached) = cache.get(&cache_key).await {
159 debug!("Cache hit for query");
160 return Ok(cached);
161 }
162 }
163 }
164
165 let req = ClientRequest {
166 query: query.to_string(),
167 user_token: user_token.to_string(),
168 client_id: self.config.client_id.clone(),
169 request_type: request_type.to_string(),
170 context,
171 media: None,
172 };
173
174 let resp = if self.config.retry.enabled && !is_mutation {
175 self.execute_with_retry(&req).await
176 } else {
177 self.execute_request(&req).await
178 };
179
180 match resp {
181 Ok(response) => {
182 if response.success && !is_mutation {
183 if let Some(cache) = &self.cache {
184 let cache_key =
185 self.build_cache_key(request_type, query, user_token, &req.context);
186 cache.insert(cache_key, response.clone()).await;
187 }
188 }
189 Ok(response)
190 }
191 Err(e) => {
192 if self.config.mode == Mode::Production && e.is_fail_open_eligible() {
193 debug!("AxonFlow unavailable, failing open: {}", e);
194 Ok(ClientResponse::fail_open(e))
195 } else {
196 Err(e)
197 }
198 }
199 }
200 }
201
202 pub async fn list_connectors(
207 &self,
208 ) -> Result<Vec<crate::types::agent::ConnectorMetadata>, AxonFlowError> {
209 let url = format!("{}/api/v1/connectors", self.config.endpoint);
210 let resp = self.checked_get(&url).await?;
211
212 let body: serde_json::Value = resp.json().await?;
213 let connectors = body["connectors"]
214 .as_array()
215 .ok_or_else(|| AxonFlowError::ApiError {
216 status: 200,
217 message: "response missing 'connectors' field".to_string(),
218 })?;
219
220 let result = serde_json::from_value(serde_json::Value::Array(connectors.clone()))?;
221 Ok(result)
222 }
223
224 pub async fn get_connector(
225 &self,
226 connector_id: &str,
227 ) -> Result<crate::types::agent::ConnectorMetadata, AxonFlowError> {
228 let encoded_id = utf8_percent_encode(connector_id, PATH_SEGMENT);
229 let url = format!("{}/api/v1/connectors/{}", self.config.endpoint, encoded_id);
230 let resp = self.checked_get(&url).await?;
231 Ok(resp.json().await?)
232 }
233
234 pub async fn get_connector_health(
235 &self,
236 connector_id: &str,
237 ) -> Result<crate::types::agent::ConnectorHealthStatus, AxonFlowError> {
238 let encoded_id = utf8_percent_encode(connector_id, PATH_SEGMENT);
239 let url = format!(
240 "{}/api/v1/connectors/{}/health",
241 self.config.endpoint, encoded_id
242 );
243 let resp = self.checked_get(&url).await?;
244 Ok(resp.json().await?)
245 }
246
247 pub async fn install_connector(
248 &self,
249 req: crate::types::agent::ConnectorInstallRequest,
250 ) -> Result<(), AxonFlowError> {
251 let encoded_id = utf8_percent_encode(&req.connector_id, PATH_SEGMENT);
252 let url = format!(
253 "{}/api/v1/connectors/{}/install",
254 self.config.endpoint, encoded_id
255 );
256 let resp = self.http_client.post(&url).json(&req).send().await?;
257 Self::check_status(resp).await?;
258 Ok(())
259 }
260
261 pub async fn query_connector(
262 &self,
263 user_token: &str,
264 connector_name: &str,
265 query: &str,
266 params: HashMap<String, serde_json::Value>,
267 ) -> Result<crate::types::agent::ConnectorResponse, AxonFlowError> {
268 let mut context = HashMap::new();
272 context.insert("connector".to_string(), serde_json::json!(connector_name));
273 context.insert("params".to_string(), serde_json::json!(params));
274
275 let resp = self
276 .proxy_llm_call(user_token, query, "mcp-query", context)
277 .await?;
278
279 Ok(crate::types::agent::ConnectorResponse {
280 success: resp.success,
281 data: resp.data.unwrap_or(serde_json::Value::Null),
282 error: resp.error,
283 meta: resp.metadata,
284 redacted: false,
285 redacted_fields: Vec::new(),
286 policy_info: None,
287 })
288 }
289
290 #[tracing::instrument(skip(self))]
295 pub async fn generate_plan(
296 &self,
297 query: &str,
298 domain: &str,
299 user_token: Option<&str>,
300 ) -> Result<crate::types::agent::PlanResponse, AxonFlowError> {
301 let mut context = HashMap::new();
302 context.insert("domain".to_string(), serde_json::json!(domain));
303 let user_token = user_token.unwrap_or("anonymous");
304
305 let resp = self
306 .proxy_llm_call(user_token, query, "generate-plan", context)
307 .await?;
308
309 if let Some(data) = resp.data {
310 let plan: crate::types::agent::PlanResponse = serde_json::from_value(data)?;
311 Ok(plan)
312 } else {
313 Err(AxonFlowError::ApiError {
314 status: 500,
315 message: "empty plan data".to_string(),
316 })
317 }
318 }
319
320 pub async fn execute_plan(
321 &self,
322 plan_id: &str,
323 user_token: Option<&str>,
324 ) -> Result<crate::types::agent::PlanExecutionResponse, AxonFlowError> {
325 let mut context = HashMap::new();
326 context.insert("plan_id".to_string(), serde_json::json!(plan_id));
327 let user_token = user_token.unwrap_or("anonymous");
328
329 let resp = self
330 .proxy_llm_call(user_token, "", "execute-plan", context)
331 .await?;
332
333 if let Some(data) = resp.data {
334 let mut exec: crate::types::agent::PlanExecutionResponse =
335 serde_json::from_value(data)?;
336 if exec.status.is_empty() {
341 exec.status = if resp.success && !resp.blocked {
342 "completed".to_string()
343 } else {
344 "failed".to_string()
345 };
346 }
347 if exec.error.is_none() {
348 exec.error = resp.error;
349 }
350 Ok(exec)
351 } else {
352 Err(AxonFlowError::ApiError {
353 status: 500,
354 message: "empty execution data".to_string(),
355 })
356 }
357 }
358
359 pub async fn get_plan_status(
360 &self,
361 plan_id: &str,
362 ) -> Result<crate::types::agent::PlanExecutionResponse, AxonFlowError> {
363 let encoded_id = utf8_percent_encode(plan_id, PATH_SEGMENT);
364 let url = format!("{}/api/v1/plan/{}", self.config.endpoint, encoded_id);
365 let resp = self.checked_map_get(&url).await?;
366 Ok(resp.json().await?)
367 }
368
369 pub async fn cancel_plan(
370 &self,
371 plan_id: &str,
372 reason: Option<&str>,
373 ) -> Result<crate::types::agent::CancelPlanResponse, AxonFlowError> {
374 let req_body = serde_json::json!({
375 "reason": reason.unwrap_or("user_cancelled"),
376 });
377
378 let encoded_id = utf8_percent_encode(plan_id, PATH_SEGMENT);
379 let url = format!("{}/api/v1/plan/{}/cancel", self.config.endpoint, encoded_id);
380 let resp = self
381 .map_http_client
382 .post(&url)
383 .json(&req_body)
384 .send()
385 .await?;
386 let resp = Self::check_status(resp).await?;
387 Ok(resp.json().await?)
388 }
389
390 pub async fn audit_llm_call(
391 &self,
392 req: &crate::types::agent::AuditRequest,
393 ) -> Result<crate::types::agent::AuditResult, AxonFlowError> {
394 let client_id = self.get_effective_client_id();
395
396 let mut req_body = serde_json::to_value(req)?;
397 req_body["client_id"] = serde_json::json!(client_id);
398 if req_body.get("metadata").map_or(true, |v| v.is_null()) {
400 req_body["metadata"] = serde_json::json!({});
401 }
402
403 let url = format!("{}/api/audit/llm-call", self.config.endpoint);
404 let resp = self.http_client.post(&url).json(&req_body).send().await?;
405
406 let status = resp.status();
407 let body = resp.text().await?;
408
409 if status.is_success() {
410 let audit_resp: crate::types::agent::AuditResult = serde_json::from_str(&body)?;
411 Ok(audit_resp)
412 } else {
413 Err(AxonFlowError::ApiError {
414 status: status.as_u16(),
415 message: body,
416 })
417 }
418 }
419
420 fn get_effective_client_id(&self) -> String {
425 self.config
426 .client_id
427 .clone()
428 .unwrap_or_else(|| "community".to_string())
429 }
430
431 fn build_cache_key(
432 &self,
433 request_type: &str,
434 query: &str,
435 user_token: &str,
436 context: &HashMap<String, serde_json::Value>,
437 ) -> String {
438 use std::hash::{Hash, Hasher};
439 let mut hasher = std::collections::hash_map::DefaultHasher::new();
440 request_type.hash(&mut hasher);
441 query.hash(&mut hasher);
442 user_token.hash(&mut hasher);
443 if !context.is_empty() {
444 let sorted: std::collections::BTreeMap<_, _> = context.iter().collect();
445 serde_json::to_string(&sorted)
446 .unwrap_or_default()
447 .hash(&mut hasher);
448 }
449 format!("{:x}", hasher.finish())
450 }
451
452 pub(crate) fn endpoint(&self) -> &str {
456 &self.config.endpoint
457 }
458
459 pub(crate) async fn checked_get(&self, url: &str) -> Result<reqwest::Response, AxonFlowError> {
460 let resp = self.http_client.get(url).send().await?;
461 Self::check_status(resp).await
462 }
463
464 pub(crate) async fn checked_post_json<T: serde::Serialize + ?Sized>(
471 &self,
472 url: &str,
473 body: &T,
474 ) -> Result<reqwest::Response, AxonFlowError> {
475 let resp = self.http_client.post(url).json(body).send().await?;
476 Self::check_status(resp).await
477 }
478
479 pub(crate) async fn raw_get(&self, url: &str) -> Result<reqwest::Response, AxonFlowError> {
485 Ok(self.http_client.get(url).send().await?)
486 }
487
488 async fn checked_map_get(&self, url: &str) -> Result<reqwest::Response, AxonFlowError> {
489 let resp = self.map_http_client.get(url).send().await?;
490 Self::check_status(resp).await
491 }
492
493 async fn check_status(resp: reqwest::Response) -> Result<reqwest::Response, AxonFlowError> {
494 if resp.status().is_success() {
495 Ok(resp)
496 } else {
497 let status = resp.status().as_u16();
498 let message = resp.text().await?;
499 Err(AxonFlowError::ApiError { status, message })
500 }
501 }
502
503 async fn execute_with_retry(
535 &self,
536 req: &ClientRequest,
537 ) -> Result<ClientResponse, AxonFlowError> {
538 let mut last_err = None;
539
540 for attempt in 0..self.config.retry.max_attempts {
541 if attempt > 0 {
542 let delay =
543 self.config.retry.initial_delay.as_secs_f64() * 2f64.powi((attempt - 1) as i32);
544 tokio::time::sleep(Duration::from_secs_f64(delay)).await;
545 }
546
547 match self.execute_request(req).await {
548 Ok(resp) => return Ok(resp),
549 Err(e) => {
550 if let AxonFlowError::ApiError { status, .. } = &e {
551 if *status >= 400
563 && *status < 500
564 && *status != 429
565 && *status != 402
566 && *status != 403
567 {
568 return Err(e);
569 }
570 }
571 last_err = Some(e);
572 }
573 }
574 }
575
576 Err(last_err.unwrap_or_else(|| {
577 AxonFlowError::ConfigError("retry loop completed with no attempts".to_string())
578 }))
579 }
580
581 async fn execute_request(&self, req: &ClientRequest) -> Result<ClientResponse, AxonFlowError> {
582 let url = format!("{}/api/request", self.config.endpoint);
583 let resp = self.http_client.post(&url).json(req).send().await?;
584
585 let status = resp.status();
586 let body = resp.text().await?;
587
588 if status.is_success() || status.as_u16() == 402 || status.as_u16() == 403 {
589 match serde_json::from_str::<ClientResponse>(&body) {
590 Ok(r) => Ok(r),
591 Err(_) => Err(AxonFlowError::ApiError {
592 status: status.as_u16(),
593 message: body,
594 }),
595 }
596 } else {
597 Err(AxonFlowError::ApiError {
598 status: status.as_u16(),
599 message: body,
600 })
601 }
602 }
603}