use reqwest::Client;
use serde::de::DeserializeOwned;
use serde_json::json;
use uuid::Uuid;
use crate::error::Error;
use crate::models::*;
const DEFAULT_BASE_URL: &str = "https://axoniac.com";
pub struct Axoniac {
api_key: String,
base_url: String,
client: Client,
}
impl Axoniac {
pub fn new(api_key: &str, base_url: Option<&str>) -> Result<Self, Error> {
if api_key.is_empty() {
return Err(Error::Config("API key cannot be empty".into()));
}
let client = Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(Error::Request)?;
Ok(Self {
api_key: api_key.to_string(),
base_url: base_url
.unwrap_or(DEFAULT_BASE_URL)
.trim_end_matches('/')
.to_string(),
client,
})
}
async fn request<T: DeserializeOwned>(
&self,
method: reqwest::Method,
path: &str,
body: Option<serde_json::Value>,
) -> Result<T, Error> {
let url = format!("{}{}", self.base_url, path);
tracing::debug!("{} {}", method, url);
let mut req = self.client.request(method, &url).bearer_auth(&self.api_key);
if let Some(body) = body {
req = req.json(&body);
}
let resp = req.send().await?;
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
let message = serde_json::from_str::<serde_json::Value>(&text)
.ok()
.and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from))
.unwrap_or(text);
return Err(Error::Api {
status: status.as_u16(),
message,
});
}
let body = resp.text().await?;
serde_json::from_str(&body).map_err(Error::Json)
}
async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
self.request(reqwest::Method::GET, path, None).await
}
async fn post<T: DeserializeOwned>(
&self,
path: &str,
body: serde_json::Value,
) -> Result<T, Error> {
self.request(reqwest::Method::POST, path, Some(body)).await
}
#[allow(dead_code)]
async fn delete_req(&self, path: &str) -> Result<serde_json::Value, Error> {
self.request(reqwest::Method::DELETE, path, None).await
}
pub async fn provision_pack(
&self,
request: ProvisionPackRequest,
) -> Result<ProvisionResult, Error> {
self.post(
"/api/service/packs/provision",
serde_json::to_value(request).map_err(Error::Json)?,
)
.await
}
pub async fn install_agent_pack(
&self,
tenant_id: &Uuid,
content_hash: &str,
) -> Result<AgentPackInstallation, Error> {
self.post(
&format!("/api/service/tenants/{}/agent-packs/install", tenant_id),
json!({ "content_hash": content_hash }),
)
.await
}
pub async fn create_tenant(
&self,
name: Option<&str>,
) -> Result<Tenant, Error> {
self.post(
"/api/service/tenants/create",
json!({ "name": name }),
)
.await
}
pub async fn get_tenant_bundle(
&self,
tenant_id: &Uuid,
) -> Result<AgentPackBundle, Error> {
self.get(&format!("/api/service/tenants/{}/bundle", tenant_id))
.await
}
pub async fn search_agent_packs(
&self,
q: Option<&str>,
category: Option<&str>,
tag: Option<&str>,
sort: Option<&str>,
) -> Result<Vec<AgentPackWithStats>, Error> {
let mut params = Vec::new();
if let Some(v) = q { params.push(format!("q={}", v)); }
if let Some(v) = category { params.push(format!("category={}", v)); }
if let Some(v) = tag { params.push(format!("tag={}", v)); }
if let Some(v) = sort { params.push(format!("sort={}", v)); }
let qs = if params.is_empty() { String::new() } else { format!("?{}", params.join("&")) };
self.get(&format!("/api/packs/catalog/search{}", qs)).await
}
pub async fn search_skills(
&self,
q: Option<&str>,
tag: Option<&str>,
sort: Option<&str>,
) -> Result<Vec<Skill>, Error> {
let mut params = Vec::new();
if let Some(v) = q { params.push(format!("q={}", v)); }
if let Some(v) = tag { params.push(format!("tag={}", v)); }
if let Some(v) = sort { params.push(format!("sort={}", v)); }
let qs = if params.is_empty() { String::new() } else { format!("?{}", params.join("&")) };
self.get(&format!("/api/packs/skills/search{}", qs)).await
}
pub async fn list_memories(
&self,
tenant_id: &Uuid,
limit: Option<u32>,
offset: Option<u32>,
) -> Result<Vec<Memory>, Error> {
let mut path = format!("/api/service/tenants/{}/memories", tenant_id);
let mut params = Vec::new();
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
if let Some(o) = offset {
params.push(format!("offset={}", o));
}
if !params.is_empty() {
path.push('?');
path.push_str(¶ms.join("&"));
}
self.get(&path).await
}
}