use std::sync::Arc;
use axum::extract::{Request, State};
use axum::http::StatusCode;
use holographic_memory::core::memory_api::MemoryError;
use serde_json::{json, Value};
use crate::auth::Scope;
use crate::config::{MAX_ID_BYTES, MAX_TEXT_BYTES};
use crate::documents::{check_metadata_size, dense, enforce_quota};
use crate::error::{ApiError, ApiResult};
use crate::request::{json_write, parse};
use crate::tenants::AppState;
fn validate_memory_request(op: &str, request: &Value, state: &AppState) -> Result<(), ApiError> {
let cfg = &state.cfg;
let vector = |value: &Value, what: &str| -> Result<(), ApiError> {
match value {
Value::Null => Ok(()),
Value::Array(items) => {
let values: Option<Vec<f64>> = items.iter().map(Value::as_f64).collect();
dense(
&values.ok_or_else(|| ApiError::bad(format!("{what} must be numbers")))?,
cfg.input_dim,
what,
)
}
_ => Err(ApiError::bad(format!("{what} must be an array of numbers"))),
}
};
let fact = |fact: &Value| -> Result<(), ApiError> {
let Value::Object(map) = fact else {
return Err(ApiError::bad("a fact must be a JSON object"));
};
if let Some(Value::String(id)) = map.get("id") {
if id.len() > MAX_ID_BYTES {
return Err(ApiError::bad(format!("id exceeds {MAX_ID_BYTES} bytes")));
}
}
if let Some(Value::String(text)) = map.get("text") {
if text.len() > MAX_TEXT_BYTES {
return Err(ApiError::bad(format!(
"text exceeds {MAX_TEXT_BYTES} bytes"
)));
}
}
if let Some(metadata) = map.get("metadata") {
check_metadata_size(metadata)?;
}
map.get("embedding")
.map_or(Ok(()), |v| vector(v, "embedding"))
};
match op {
"remember" => match request.get("facts") {
Some(Value::Array(facts)) => {
if facts.is_empty() || facts.len() > cfg.max_batch {
return Err(ApiError::bad(format!(
"facts must contain 1..={} entries",
cfg.max_batch
)));
}
facts.iter().try_for_each(fact)
}
Some(_) => Err(ApiError::bad("facts must be an array")),
None => fact(request),
},
"recall" | "explain" => {
if let Some(Value::String(query)) = request.get("query") {
if query.len() > 8192 {
return Err(ApiError::bad("query requires at most 8192 bytes"));
}
}
if let Some(k) = request.get("k").and_then(Value::as_i64) {
if k < 1 || k > i64::from(cfg.max_top_k) {
return Err(ApiError::bad(format!("k must be 1..={}", cfg.max_top_k)));
}
}
request
.get("embedding")
.map_or(Ok(()), |v| vector(v, "embedding"))
}
_ => Ok(()),
}
}
fn memory_error(error: anyhow::Error) -> ApiError {
match error.downcast_ref::<MemoryError>() {
Some(MemoryError::Invalid(message)) => ApiError::bad(message.clone()),
Some(MemoryError::NotFound(message)) => ApiError::not_found(message.clone()),
Some(MemoryError::Conflict(message)) => ApiError::conflict(message.clone()),
None => ApiError::internal(error),
}
}
fn new_facts(request: &Value, exists: impl Fn(&str) -> bool) -> u64 {
let one = |fact: &Value| !fact.get("id").and_then(Value::as_str).is_some_and(&exists);
match request.get("facts").and_then(Value::as_array) {
Some(facts) => facts.iter().filter(|f| one(f)).count() as u64,
None => u64::from(one(request)),
}
}
async fn run(op: &'static str, scope: Scope, state: Arc<AppState>, req: Request) -> ApiResult {
json_write(state, req, scope, move |state, tenant, bytes| {
let request: Value = if bytes.is_empty() {
json!({})
} else {
parse(&bytes)?
};
validate_memory_request(op, &request, state)?;
let core = state.core(tenant)?;
if op == "remember" {
enforce_quota(state, &core, || {
new_facts(&request, |id| core.has_document(id))
})?;
}
let response = core.memory().call(op, request).map_err(memory_error)?;
if scope == Scope::Write {
core.bump();
}
Ok((StatusCode::OK, response))
})
.await
}
macro_rules! memory_handler {
($name:ident, $op:literal, $scope:expr) => {
pub async fn $name(State(state): State<Arc<AppState>>, req: Request) -> ApiResult {
run($op, $scope, state, req).await
}
};
}
memory_handler!(remember, "remember", Scope::Write);
memory_handler!(recall, "recall", Scope::Read);
memory_handler!(explain, "explain", Scope::Read);
memory_handler!(forget, "forget", Scope::Write);
memory_handler!(get, "get", Scope::Read);
memory_handler!(history, "history", Scope::Read);
memory_handler!(apply_forgetting, "applyForgetting", Scope::Write);
memory_handler!(consolidate, "consolidate", Scope::Write);