1use std::time::Duration;
9
10use serde::de::DeserializeOwned;
11use serde::Serialize;
12
13use super::error::BrainHttpError;
14use super::retry::HttpRetryPolicy;
15use super::types::{
16 Capabilities, CreateEntityInput, CreateEntityResult, EncodeInput, EncodeResult, EntityDetail,
17 ForgetInput, ForgetResult, GetRelationQuery, GetStatementQuery, GraphFetchQuery, GraphPage,
18 LinkInput, LinkResult, ListEntitiesQuery, ListEntitiesResult, ListRelationsQuery,
19 ListRelationsResult, ListStatementsQuery, ListStatementsResult, MemoryInspect, MemoryListPage,
20 MemoryListQuery, PlanInput, PlanResult, ReasonInput, ReasonResult, RecallInput, RecallResult,
21 RelationDetail, ResolveEntityInput, ResolveEntityResult, Schema, SchemaGetQuery,
22 SchemaReplaceInput, SchemaReplaceResult, SchemaUploadInput, SchemaUploadResult,
23 SchemaValidateInput, SchemaValidateResult, StatementDetail, TraverseInput, TraverseResult,
24 UnlinkInput, UnlinkResult, Whoami,
25};
26
27pub const DEFAULT_BASE_URL: &str = "http://127.0.0.1:8080";
29
30pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
32
33fn build_http(timeout: Duration) -> reqwest::Client {
37 reqwest::Client::builder()
38 .timeout(timeout)
39 .build()
40 .unwrap_or_else(|_| reqwest::Client::new())
41}
42
43fn segment(value: &str) -> String {
50 let mut out = String::with_capacity(value.len());
51 for &b in value.as_bytes() {
52 match b {
53 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'~' => out.push(b as char),
54 _ => {
57 use std::fmt::Write as _;
58 let _ = write!(out, "%{b:02X}");
59 }
60 }
61 }
62 out
63}
64
65fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
68 headers
69 .get(reqwest::header::RETRY_AFTER)
70 .and_then(|v| v.to_str().ok())
71 .and_then(|s| s.trim().parse::<u64>().ok())
72 .map(Duration::from_secs)
73}
74
75#[derive(Clone, Debug)]
77pub struct BrainHttpClient {
78 base: String,
79 api_key: String,
80 http: reqwest::Client,
81 retry: HttpRetryPolicy,
82}
83
84impl BrainHttpClient {
85 #[must_use]
95 pub fn new(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
96 let base = base_url.into().trim_end_matches('/').to_string();
97 Self {
98 base,
99 api_key: api_key.into(),
100 http: build_http(DEFAULT_TIMEOUT),
101 retry: HttpRetryPolicy::default(),
102 }
103 }
104
105 #[must_use]
107 pub fn localhost(api_key: impl Into<String>) -> Self {
108 Self::new(api_key, DEFAULT_BASE_URL)
109 }
110
111 #[must_use]
114 pub fn with_timeout(mut self, timeout: Duration) -> Self {
115 self.http = build_http(timeout);
116 self
117 }
118
119 #[must_use]
122 pub fn with_retry_policy(mut self, policy: HttpRetryPolicy) -> Self {
123 self.retry = policy;
124 self
125 }
126
127 pub async fn encode(&self, input: &EncodeInput) -> Result<EncodeResult, BrainHttpError> {
129 self.send(reqwest::Method::POST, "/v1/memories", Some(input), true)
130 .await
131 }
132
133 pub async fn recall(&self, input: &RecallInput) -> Result<RecallResult, BrainHttpError> {
139 self.send(reqwest::Method::POST, "/v1/recall", Some(input), true)
140 .await
141 }
142
143 pub async fn forget(&self, input: &ForgetInput) -> Result<ForgetResult, BrainHttpError> {
145 self.send(reqwest::Method::DELETE, "/v1/memories", Some(input), true)
148 .await
149 }
150
151 pub async fn link(&self, input: &LinkInput) -> Result<LinkResult, BrainHttpError> {
153 self.send(reqwest::Method::POST, "/v1/links", Some(input), false)
154 .await
155 }
156
157 pub async fn unlink(&self, input: &UnlinkInput) -> Result<UnlinkResult, BrainHttpError> {
159 self.send(reqwest::Method::DELETE, "/v1/links", Some(input), true)
162 .await
163 }
164
165 pub async fn plan(&self, input: &PlanInput) -> Result<PlanResult, BrainHttpError> {
167 self.send(reqwest::Method::POST, "/v1/plan", Some(input), true)
168 .await
169 }
170
171 pub async fn reason(&self, input: &ReasonInput) -> Result<ReasonResult, BrainHttpError> {
173 self.send(reqwest::Method::POST, "/v1/reason", Some(input), true)
174 .await
175 }
176
177 pub async fn whoami(&self) -> Result<Whoami, BrainHttpError> {
179 self.send::<(), _>(reqwest::Method::GET, "/v1/whoami", None, true)
180 .await
181 }
182
183 pub async fn capabilities(&self) -> Result<Capabilities, BrainHttpError> {
185 self.send::<(), _>(reqwest::Method::GET, "/v1/capabilities", None, true)
186 .await
187 }
188
189 pub async fn memory_list(
200 &self,
201 query: &MemoryListQuery,
202 ) -> Result<MemoryListPage, BrainHttpError> {
203 self.send_with_query::<(), _, _>(
204 reqwest::Method::GET,
205 "/v1/memories",
206 Some(query),
207 None,
208 true,
209 )
210 .await
211 }
212
213 pub async fn memory_inspect(&self, memory_id: &str) -> Result<MemoryInspect, BrainHttpError> {
216 let id = segment(memory_id);
217 self.send::<(), _>(
218 reqwest::Method::GET,
219 &format!("/v1/memories/{id}/inspect"),
220 None,
221 true,
222 )
223 .await
224 }
225
226 pub async fn list_entities(
228 &self,
229 query: &ListEntitiesQuery,
230 ) -> Result<ListEntitiesResult, BrainHttpError> {
231 self.send_with_query::<(), _, _>(
232 reqwest::Method::GET,
233 "/v1/entities",
234 Some(query),
235 None,
236 true,
237 )
238 .await
239 }
240
241 pub async fn get_entity(&self, entity_id: &str) -> Result<EntityDetail, BrainHttpError> {
243 let id = segment(entity_id);
244 self.send::<(), _>(
245 reqwest::Method::GET,
246 &format!("/v1/entities/{id}"),
247 None,
248 true,
249 )
250 .await
251 }
252
253 pub async fn list_relations(
255 &self,
256 entity_id: &str,
257 query: &ListRelationsQuery,
258 ) -> Result<ListRelationsResult, BrainHttpError> {
259 let id = segment(entity_id);
260 self.send_with_query::<(), _, _>(
261 reqwest::Method::GET,
262 &format!("/v1/entities/{id}/relations"),
263 Some(query),
264 None,
265 true,
266 )
267 .await
268 }
269
270 pub async fn graph_fetch(&self, query: &GraphFetchQuery) -> Result<GraphPage, BrainHttpError> {
272 self.send_with_query::<(), _, _>(reqwest::Method::GET, "/v1/graph", Some(query), None, true)
273 .await
274 }
275
276 pub async fn get_relation(
278 &self,
279 relation_id: &str,
280 query: &GetRelationQuery,
281 ) -> Result<RelationDetail, BrainHttpError> {
282 let id = segment(relation_id);
283 self.send_with_query::<(), _, _>(
284 reqwest::Method::GET,
285 &format!("/v1/relations/{id}"),
286 Some(query),
287 None,
288 true,
289 )
290 .await
291 }
292
293 pub async fn list_statements(
295 &self,
296 query: &ListStatementsQuery,
297 ) -> Result<ListStatementsResult, BrainHttpError> {
298 self.send_with_query::<(), _, _>(
299 reqwest::Method::GET,
300 "/v1/statements",
301 Some(query),
302 None,
303 true,
304 )
305 .await
306 }
307
308 pub async fn get_statement(
310 &self,
311 statement_id: &str,
312 query: &GetStatementQuery,
313 ) -> Result<StatementDetail, BrainHttpError> {
314 let id = segment(statement_id);
315 self.send_with_query::<(), _, _>(
316 reqwest::Method::GET,
317 &format!("/v1/statements/{id}"),
318 Some(query),
319 None,
320 true,
321 )
322 .await
323 }
324
325 pub async fn get_schema(&self, query: &SchemaGetQuery) -> Result<Schema, BrainHttpError> {
327 self.send_with_query::<(), _, _>(
328 reqwest::Method::GET,
329 "/v1/schema",
330 Some(query),
331 None,
332 true,
333 )
334 .await
335 }
336
337 pub async fn validate_schema(
355 &self,
356 input: &SchemaValidateInput,
357 ) -> Result<SchemaValidateResult, BrainHttpError> {
358 self.send(
359 reqwest::Method::POST,
360 "/v1/schema/validate",
361 Some(input),
362 true,
363 )
364 .await
365 }
366
367 pub async fn upload_schema(
369 &self,
370 input: &SchemaUploadInput,
371 ) -> Result<SchemaUploadResult, BrainHttpError> {
372 self.send(reqwest::Method::POST, "/v1/schema", Some(input), false)
373 .await
374 }
375
376 pub async fn replace_schema(
380 &self,
381 input: &SchemaReplaceInput,
382 ) -> Result<SchemaReplaceResult, BrainHttpError> {
383 self.send(reqwest::Method::PUT, "/v1/schema", Some(input), false)
384 .await
385 }
386
387 pub async fn create_entity(
398 &self,
399 input: &CreateEntityInput,
400 ) -> Result<CreateEntityResult, BrainHttpError> {
401 self.send(reqwest::Method::POST, "/v1/entities", Some(input), false)
402 .await
403 }
404
405 pub async fn resolve_entity(
408 &self,
409 input: &ResolveEntityInput,
410 ) -> Result<ResolveEntityResult, BrainHttpError> {
411 self.send(
412 reqwest::Method::POST,
413 "/v1/entities/resolve",
414 Some(input),
415 false,
416 )
417 .await
418 }
419
420 pub async fn traverse_relations(
422 &self,
423 entity_id: &str,
424 input: &TraverseInput,
425 ) -> Result<TraverseResult, BrainHttpError> {
426 let id = segment(entity_id);
427 self.send(
428 reqwest::Method::POST,
429 &format!("/v1/entities/{id}/traverse"),
430 Some(input),
431 true,
432 )
433 .await
434 }
435
436 async fn send<B, R>(
439 &self,
440 method: reqwest::Method,
441 path: &str,
442 body: Option<&B>,
443 idempotent: bool,
444 ) -> Result<R, BrainHttpError>
445 where
446 B: Serialize + ?Sized,
447 R: DeserializeOwned,
448 {
449 self.send_with_query::<B, (), R>(method, path, None, body, idempotent)
450 .await
451 }
452
453 async fn send_with_query<B, Q, R>(
457 &self,
458 method: reqwest::Method,
459 path: &str,
460 query: Option<&Q>,
461 body: Option<&B>,
462 idempotent: bool,
463 ) -> Result<R, BrainHttpError>
464 where
465 B: Serialize + ?Sized,
466 Q: Serialize + ?Sized,
467 R: DeserializeOwned,
468 {
469 let mut attempt = 1u32;
470 loop {
471 match self
472 .send_once::<B, Q, R>(method.clone(), path, query, body)
473 .await
474 {
475 Ok(value) => return Ok(value),
476 Err((err, retry_after)) => {
477 if idempotent && self.retry.should_retry(attempt, &err) {
478 tokio::time::sleep(self.retry.backoff(attempt, retry_after)).await;
479 attempt += 1;
480 } else {
481 return Err(err);
482 }
483 }
484 }
485 }
486 }
487
488 async fn send_once<B, Q, R>(
491 &self,
492 method: reqwest::Method,
493 path: &str,
494 query: Option<&Q>,
495 body: Option<&B>,
496 ) -> Result<R, (BrainHttpError, Option<Duration>)>
497 where
498 B: Serialize + ?Sized,
499 Q: Serialize + ?Sized,
500 R: DeserializeOwned,
501 {
502 let url = format!("{}{}", self.base, path);
503 let mut rb = self.http.request(method, &url).bearer_auth(&self.api_key);
504 if let Some(q) = query {
505 rb = rb.query(q);
506 }
507 if let Some(b) = body {
508 rb = rb.json(b);
509 }
510 let resp = rb.send().await.map_err(|e| {
511 (
512 BrainHttpError::transport(format!("request to {url} failed: {e}")),
513 None,
514 )
515 })?;
516 let status = resp.status();
517 let retry_after = parse_retry_after(resp.headers());
518 let text = resp.text().await.map_err(|e| {
519 (
520 BrainHttpError::transport(format!("read body from {url}: {e}")),
521 None,
522 )
523 })?;
524 if !status.is_success() {
525 return Err((
526 BrainHttpError::from_body(status.as_u16(), &text),
527 retry_after,
528 ));
529 }
530 serde_json::from_str(&text).map_err(|e| {
531 (
532 BrainHttpError::transport(format!("decode {url} response: {e}")),
533 None,
534 )
535 })
536 }
537}