mod error;
pub use error::{ApiError, Error, Result};
use error::ErrorEnvelope;
use serde::{Deserialize, Serialize};
const DEFAULT_BASE_URL: &str = "https://api.velixarai.com/v1";
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Memory {
pub id: String,
pub content: String,
#[serde(default)]
pub score: Option<f32>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub tier: Option<u8>,
#[serde(default)]
pub user_id: Option<String>,
#[serde(default)]
pub source_class: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct SearchResult {
#[serde(default)]
pub memories: Vec<Memory>,
#[serde(default)]
pub count: usize,
}
#[derive(Debug, Default, Clone, Serialize)]
pub struct StoreOpts {
#[serde(rename = "user_id", skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tier: Option<u8>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
}
impl StoreOpts {
pub fn new() -> Self {
Self::default()
}
pub fn user(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn tier(mut self, tier: u8) -> Self {
self.tier = Some(tier);
self
}
pub fn tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tags = tags.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Default, Clone)]
pub struct SearchOpts {
user_id: Option<String>,
limit: Option<usize>,
}
impl SearchOpts {
pub fn new() -> Self {
Self::default()
}
pub fn user(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
}
#[derive(Serialize)]
struct StoreBody<'a> {
content: &'a str,
#[serde(flatten)]
opts: StoreOpts,
}
#[derive(Deserialize)]
struct StoreResponse {
id: String,
}
#[derive(Deserialize)]
struct DeleteResponse {
#[serde(default)]
deleted: bool,
}
#[derive(Debug, Clone)]
pub struct Velixar {
http: reqwest::Client,
base_url: String,
api_key: String,
}
impl Velixar {
pub fn new(api_key: impl Into<String>) -> Result<Self> {
Self::builder(api_key).build()
}
pub fn builder(api_key: impl Into<String>) -> Builder {
Builder {
api_key: api_key.into(),
base_url: DEFAULT_BASE_URL.to_string(),
http: None,
}
}
pub async fn store(&self, content: &str, opts: StoreOpts) -> Result<String> {
let body = StoreBody { content, opts };
let res: StoreResponse = self
.send(self.req(reqwest::Method::POST, "/memory").json(&body))
.await?;
Ok(res.id)
}
pub async fn search(&self, query: &str, opts: SearchOpts) -> Result<SearchResult> {
let mut req = self
.req(reqwest::Method::GET, "/memory/search")
.query(&[("query", query)]);
if let Some(u) = &opts.user_id {
req = req.query(&[("user_id", u.as_str())]);
}
if let Some(l) = opts.limit {
req = req.query(&[("limit", l.to_string().as_str())]);
}
self.send(req).await
}
pub async fn delete(&self, id: &str) -> Result<bool> {
let res: DeleteResponse = self
.send(self.req(reqwest::Method::DELETE, &format!("/memory/{id}")))
.await?;
Ok(res.deleted)
}
pub async fn health(&self) -> Result<serde_json::Value> {
self.send(self.req(reqwest::Method::GET, "/health")).await
}
fn req(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
self.http
.request(method, format!("{}{}", self.base_url, path))
.bearer_auth(&self.api_key)
}
async fn send<T: serde::de::DeserializeOwned>(
&self,
req: reqwest::RequestBuilder,
) -> Result<T> {
let res = req.send().await?;
let status = res.status();
let text = res.text().await?;
if !status.is_success() {
return Err(match serde_json::from_str::<ErrorEnvelope>(&text) {
Ok(env) => Error::from_status(status.as_u16(), env.error),
Err(_) => Error::Api {
status: status.as_u16(),
body: ApiError {
code: format!("HTTP_{}", status.as_u16()),
message: text.chars().take(200).collect(),
},
},
});
}
serde_json::from_str(&text).map_err(|e| {
Error::Decode(format!(
"{e}: {}",
text.chars().take(200).collect::<String>()
))
})
}
}
pub struct Builder {
api_key: String,
base_url: String,
http: Option<reqwest::Client>,
}
impl Builder {
pub fn base_url(mut self, url: impl Into<String>) -> Self {
let mut u = url.into();
while u.ends_with('/') {
u.pop();
}
self.base_url = u;
self
}
pub fn http_client(mut self, http: reqwest::Client) -> Self {
self.http = Some(http);
self
}
pub fn build(self) -> Result<Velixar> {
Ok(Velixar {
http: self.http.unwrap_or_default(),
base_url: self.base_url,
api_key: self.api_key,
})
}
}