use std::time::Duration;
use serde::de::DeserializeOwned;
use serde::Serialize;
use super::error::BrainHttpError;
use super::retry::HttpRetryPolicy;
use super::types::{
Capabilities, CreateEntityInput, CreateEntityResult, EncodeInput, EncodeResult, EntityDetail,
ForgetInput, ForgetResult, GetRelationQuery, GetStatementQuery, GraphFetchQuery, GraphPage,
LinkInput, LinkResult, ListEntitiesQuery, ListEntitiesResult, ListRelationsQuery,
ListRelationsResult, ListStatementsQuery, ListStatementsResult, MemoryInspect, MemoryListPage,
MemoryListQuery, PlanInput, PlanResult, ReasonInput, ReasonResult, RecallInput, RecallResult,
RelationDetail, ResolveEntityInput, ResolveEntityResult, Schema, SchemaGetQuery,
SchemaReplaceInput, SchemaReplaceResult, SchemaUploadInput, SchemaUploadResult,
SchemaValidateInput, SchemaValidateResult, StatementDetail, TraverseInput, TraverseResult,
UnlinkInput, UnlinkResult, Whoami,
};
pub const DEFAULT_BASE_URL: &str = "http://127.0.0.1:8080";
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
fn build_http(timeout: Duration) -> reqwest::Client {
reqwest::Client::builder()
.timeout(timeout)
.build()
.unwrap_or_else(|_| reqwest::Client::new())
}
fn segment(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for &b in value.as_bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'~' => out.push(b as char),
_ => {
use std::fmt::Write as _;
let _ = write!(out, "%{b:02X}");
}
}
}
out
}
fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
headers
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.trim().parse::<u64>().ok())
.map(Duration::from_secs)
}
#[derive(Clone, Debug)]
pub struct BrainHttpClient {
base: String,
api_key: String,
http: reqwest::Client,
retry: HttpRetryPolicy,
}
impl BrainHttpClient {
#[must_use]
pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
let base = base_url.into().trim_end_matches('/').to_string();
Self {
base,
api_key: api_key.into(),
http: build_http(DEFAULT_TIMEOUT),
retry: HttpRetryPolicy::default(),
}
}
#[must_use]
pub fn localhost(api_key: impl Into<String>) -> Self {
Self::new(api_key, DEFAULT_BASE_URL)
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.http = build_http(timeout);
self
}
#[must_use]
pub fn with_retry_policy(mut self, policy: HttpRetryPolicy) -> Self {
self.retry = policy;
self
}
pub async fn encode(&self, input: &EncodeInput) -> Result<EncodeResult, BrainHttpError> {
self.send(reqwest::Method::POST, "/v1/memories", Some(input), true)
.await
}
pub async fn recall(&self, input: &RecallInput) -> Result<RecallResult, BrainHttpError> {
self.send(reqwest::Method::POST, "/v1/recall", Some(input), true)
.await
}
pub async fn forget(&self, input: &ForgetInput) -> Result<ForgetResult, BrainHttpError> {
self.send(reqwest::Method::DELETE, "/v1/memories", Some(input), true)
.await
}
pub async fn link(&self, input: &LinkInput) -> Result<LinkResult, BrainHttpError> {
self.send(reqwest::Method::POST, "/v1/links", Some(input), false)
.await
}
pub async fn unlink(&self, input: &UnlinkInput) -> Result<UnlinkResult, BrainHttpError> {
self.send(reqwest::Method::DELETE, "/v1/links", Some(input), true)
.await
}
pub async fn plan(&self, input: &PlanInput) -> Result<PlanResult, BrainHttpError> {
self.send(reqwest::Method::POST, "/v1/plan", Some(input), true)
.await
}
pub async fn reason(&self, input: &ReasonInput) -> Result<ReasonResult, BrainHttpError> {
self.send(reqwest::Method::POST, "/v1/reason", Some(input), true)
.await
}
pub async fn whoami(&self) -> Result<Whoami, BrainHttpError> {
self.send::<(), _>(reqwest::Method::GET, "/v1/whoami", None, true)
.await
}
pub async fn capabilities(&self) -> Result<Capabilities, BrainHttpError> {
self.send::<(), _>(reqwest::Method::GET, "/v1/capabilities", None, true)
.await
}
pub async fn memory_list(
&self,
query: &MemoryListQuery,
) -> Result<MemoryListPage, BrainHttpError> {
self.send_with_query::<(), _, _>(
reqwest::Method::GET,
"/v1/memories",
Some(query),
None,
true,
)
.await
}
pub async fn memory_inspect(&self, memory_id: &str) -> Result<MemoryInspect, BrainHttpError> {
let id = segment(memory_id);
self.send::<(), _>(
reqwest::Method::GET,
&format!("/v1/memories/{id}/inspect"),
None,
true,
)
.await
}
pub async fn list_entities(
&self,
query: &ListEntitiesQuery,
) -> Result<ListEntitiesResult, BrainHttpError> {
self.send_with_query::<(), _, _>(
reqwest::Method::GET,
"/v1/entities",
Some(query),
None,
true,
)
.await
}
pub async fn get_entity(&self, entity_id: &str) -> Result<EntityDetail, BrainHttpError> {
let id = segment(entity_id);
self.send::<(), _>(
reqwest::Method::GET,
&format!("/v1/entities/{id}"),
None,
true,
)
.await
}
pub async fn list_relations(
&self,
entity_id: &str,
query: &ListRelationsQuery,
) -> Result<ListRelationsResult, BrainHttpError> {
let id = segment(entity_id);
self.send_with_query::<(), _, _>(
reqwest::Method::GET,
&format!("/v1/entities/{id}/relations"),
Some(query),
None,
true,
)
.await
}
pub async fn graph_fetch(&self, query: &GraphFetchQuery) -> Result<GraphPage, BrainHttpError> {
self.send_with_query::<(), _, _>(reqwest::Method::GET, "/v1/graph", Some(query), None, true)
.await
}
pub async fn get_relation(
&self,
relation_id: &str,
query: &GetRelationQuery,
) -> Result<RelationDetail, BrainHttpError> {
let id = segment(relation_id);
self.send_with_query::<(), _, _>(
reqwest::Method::GET,
&format!("/v1/relations/{id}"),
Some(query),
None,
true,
)
.await
}
pub async fn list_statements(
&self,
query: &ListStatementsQuery,
) -> Result<ListStatementsResult, BrainHttpError> {
self.send_with_query::<(), _, _>(
reqwest::Method::GET,
"/v1/statements",
Some(query),
None,
true,
)
.await
}
pub async fn get_statement(
&self,
statement_id: &str,
query: &GetStatementQuery,
) -> Result<StatementDetail, BrainHttpError> {
let id = segment(statement_id);
self.send_with_query::<(), _, _>(
reqwest::Method::GET,
&format!("/v1/statements/{id}"),
Some(query),
None,
true,
)
.await
}
pub async fn get_schema(&self, query: &SchemaGetQuery) -> Result<Schema, BrainHttpError> {
self.send_with_query::<(), _, _>(
reqwest::Method::GET,
"/v1/schema",
Some(query),
None,
true,
)
.await
}
pub async fn validate_schema(
&self,
input: &SchemaValidateInput,
) -> Result<SchemaValidateResult, BrainHttpError> {
self.send(
reqwest::Method::POST,
"/v1/schema/validate",
Some(input),
true,
)
.await
}
pub async fn upload_schema(
&self,
input: &SchemaUploadInput,
) -> Result<SchemaUploadResult, BrainHttpError> {
self.send(reqwest::Method::POST, "/v1/schema", Some(input), false)
.await
}
pub async fn replace_schema(
&self,
input: &SchemaReplaceInput,
) -> Result<SchemaReplaceResult, BrainHttpError> {
self.send(reqwest::Method::PUT, "/v1/schema", Some(input), false)
.await
}
pub async fn create_entity(
&self,
input: &CreateEntityInput,
) -> Result<CreateEntityResult, BrainHttpError> {
self.send(reqwest::Method::POST, "/v1/entities", Some(input), false)
.await
}
pub async fn resolve_entity(
&self,
input: &ResolveEntityInput,
) -> Result<ResolveEntityResult, BrainHttpError> {
self.send(
reqwest::Method::POST,
"/v1/entities/resolve",
Some(input),
false,
)
.await
}
pub async fn traverse_relations(
&self,
entity_id: &str,
input: &TraverseInput,
) -> Result<TraverseResult, BrainHttpError> {
let id = segment(entity_id);
self.send(
reqwest::Method::POST,
&format!("/v1/entities/{id}/traverse"),
Some(input),
true,
)
.await
}
async fn send<B, R>(
&self,
method: reqwest::Method,
path: &str,
body: Option<&B>,
idempotent: bool,
) -> Result<R, BrainHttpError>
where
B: Serialize + ?Sized,
R: DeserializeOwned,
{
self.send_with_query::<B, (), R>(method, path, None, body, idempotent)
.await
}
async fn send_with_query<B, Q, R>(
&self,
method: reqwest::Method,
path: &str,
query: Option<&Q>,
body: Option<&B>,
idempotent: bool,
) -> Result<R, BrainHttpError>
where
B: Serialize + ?Sized,
Q: Serialize + ?Sized,
R: DeserializeOwned,
{
let mut attempt = 1u32;
loop {
match self
.send_once::<B, Q, R>(method.clone(), path, query, body)
.await
{
Ok(value) => return Ok(value),
Err((err, retry_after)) => {
if idempotent && self.retry.should_retry(attempt, &err) {
tokio::time::sleep(self.retry.backoff(attempt, retry_after)).await;
attempt += 1;
} else {
return Err(err);
}
}
}
}
}
async fn send_once<B, Q, R>(
&self,
method: reqwest::Method,
path: &str,
query: Option<&Q>,
body: Option<&B>,
) -> Result<R, (BrainHttpError, Option<Duration>)>
where
B: Serialize + ?Sized,
Q: Serialize + ?Sized,
R: DeserializeOwned,
{
let url = format!("{}{}", self.base, path);
let mut rb = self.http.request(method, &url).bearer_auth(&self.api_key);
if let Some(q) = query {
rb = rb.query(q);
}
if let Some(b) = body {
rb = rb.json(b);
}
let resp = rb.send().await.map_err(|e| {
(
BrainHttpError::transport(format!("request to {url} failed: {e}")),
None,
)
})?;
let status = resp.status();
let retry_after = parse_retry_after(resp.headers());
let text = resp.text().await.map_err(|e| {
(
BrainHttpError::transport(format!("read body from {url}: {e}")),
None,
)
})?;
if !status.is_success() {
return Err((
BrainHttpError::from_body(status.as_u16(), &text),
retry_after,
));
}
serde_json::from_str(&text).map_err(|e| {
(
BrainHttpError::transport(format!("decode {url} response: {e}")),
None,
)
})
}
}