use std::io::Read as IoRead;
use serde::{Deserialize, Serialize};
use tiny_http::Header;
#[derive(Deserialize)]
pub struct DocCreateRequest {
pub slug: String,
pub title: Option<String>,
pub content: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub parent: Option<String>,
}
#[derive(Deserialize)]
pub struct DocGetRequest {
pub id: Option<String>,
pub slug: Option<String>,
#[serde(default)]
pub namespace: Option<String>,
}
#[derive(Deserialize)]
pub struct DocListParams {
#[serde(default)]
pub namespace: Option<String>,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default)]
pub roots_only: bool,
#[serde(default)]
pub parent: Option<String>,
}
#[derive(Deserialize)]
pub struct DocSearchRequest {
pub query: String,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default = "default_search_mode")]
pub mode: String,
}
#[derive(Deserialize)]
pub struct DocMoveRequest {
pub id: Option<String>,
pub slug: Option<String>,
#[serde(default)]
pub new_parent: Option<String>,
#[serde(default)]
pub namespace: Option<String>,
}
pub fn default_search_mode() -> String {
"hybrid".to_string()
}
pub fn resolve_doc_id(req: &DocGetRequest) -> Result<&str, &'static str> {
match (&req.id, &req.slug) {
(Some(id), _) => Ok(id),
(_, Some(slug)) => Ok(slug),
_ => Err("provide either 'id' or 'slug'"),
}
}
pub fn resolve_doc_id_move(req: &DocMoveRequest) -> Result<&str, &'static str> {
match (&req.id, &req.slug) {
(Some(id), _) => Ok(id),
(_, Some(slug)) => Ok(slug),
_ => Err("provide either 'id' or 'slug'"),
}
}
#[derive(Deserialize)]
pub struct RememberRequest {
pub content: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub r#type: Option<String>,
#[serde(default)]
pub valid_from: Option<String>,
#[serde(default)]
pub valid_until: Option<String>,
#[serde(default)]
pub detect_contradiction: bool,
}
#[derive(Deserialize)]
pub struct RecallRequest {
pub query: String,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub entity: Option<String>,
#[serde(default)]
pub category: Option<String>,
#[serde(default)]
pub min_score: Option<f32>,
#[serde(default)]
pub strict: bool,
#[serde(default)]
pub at: Option<String>,
#[serde(default)]
pub search_type: Option<String>,
}
#[derive(Deserialize)]
pub struct SearchRequest {
pub query: String,
#[serde(default = "default_limit_search")]
pub limit: usize,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub namespace: Option<String>,
}
#[derive(Deserialize)]
pub struct ListParams {
#[serde(default)]
pub tag: Option<String>,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default)]
pub offset: usize,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default)]
pub at: Option<String>,
}
#[derive(Serialize)]
pub struct HealthResponse {
pub status: &'static str,
pub memories: usize,
pub namespaces: usize,
}
#[derive(Serialize)]
pub struct ErrorResponse {
pub error: String,
}
pub fn default_limit() -> usize {
5
}
#[derive(Deserialize)]
pub struct RoomRecallRequest {
pub room_id: String,
pub query: String,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub min_score: Option<f32>,
}
pub fn default_limit_search() -> usize {
10
}
pub fn json_header() -> Header {
Header::from_bytes("Content-Type", "application/json").unwrap()
}
pub fn read_body<T: serde::de::DeserializeOwned>(reader: &mut dyn IoRead) -> Result<T, String> {
let mut limited = reader.take(uteke_core::MAX_PAYLOAD_SIZE as u64 + 1);
let mut body = String::new();
limited
.read_to_string(&mut body)
.map_err(|e| format!("Failed to read body: {e}"))?;
if body.len() > uteke_core::MAX_PAYLOAD_SIZE {
return Err(format!(
"Payload too large: {} bytes (max {})",
body.len(),
uteke_core::MAX_PAYLOAD_SIZE
));
}
serde_json::from_str(&body).map_err(|e| format!("Invalid JSON: {e}"))
}
pub fn url_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut decoded: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'+' => {
decoded.push(b' ');
}
b'%' if i + 2 < bytes.len() => {
let hex = &s[i + 1..i + 3];
if let Ok(byte) = u8::from_str_radix(hex, 16) {
decoded.push(byte);
i += 2;
} else {
decoded.push(b'%');
}
}
c => decoded.push(c),
}
i += 1;
}
String::from_utf8(decoded).unwrap_or_else(|_| s.to_string())
}
pub fn parse_query_param(query: &str, key: &str) -> Option<String> {
query.split('&').find_map(|pair| {
let mut kv = pair.splitn(2, '=');
if kv.next()? == key {
Some(url_decode(kv.next()?))
} else {
None
}
})
}
pub fn parse_query_namespace(path: &str) -> Option<String> {
let query = path.split('?').nth(1)?;
parse_query_param(query, "namespace")
}
pub fn default_namespace() -> String {
"default".to_string()
}
pub fn ns(ns: &Option<String>) -> Option<&str> {
ns.as_deref()
}
#[derive(serde::Deserialize, Default, Clone)]
pub struct RecallFileSection {
pub min_score: Option<f64>,
pub min_score_strict: Option<f64>,
}
pub const STRICT_THRESHOLD: f32 = 0.5;
pub const DEFAULT_MIN_SCORE: f32 = 0.0;