#![allow(clippy::missing_safety_doc)]
use crate::db::AgentDB;
use crate::hybrid::HybridQuery;
use crate::vectors::{DistanceMetric, SearchOptions, VectorEntry};
use serde_json::Value;
use std::cell::RefCell;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
thread_local! {
static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None);
}
fn set_last_error(msg: impl Into<String>) {
LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg.into()));
}
fn clear_last_error() {
LAST_ERROR.with(|e| *e.borrow_mut() = None);
}
#[no_mangle]
pub extern "C" fn agentdb_last_error() -> *mut c_char {
LAST_ERROR.with(|e| match e.borrow().as_deref() {
Some(msg) => CString::new(msg)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut()),
None => std::ptr::null_mut(),
})
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
let _ = CString::from_raw(ptr);
}
}
pub struct AgentDbHandle {
db: AgentDB,
}
#[no_mangle]
pub extern "C" fn agentdb_open(path: *const c_char) -> *mut AgentDbHandle {
clear_last_error();
let path_str = unsafe {
match path.as_ref().and_then(|p| CStr::from_ptr(p).to_str().ok()) {
Some(s) => s,
None => {
set_last_error("agentdb_open: invalid path string");
return std::ptr::null_mut();
}
}
};
match AgentDB::open(path_str) {
Ok(db) => Box::into_raw(Box::new(AgentDbHandle { db })),
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_close(handle: *mut AgentDbHandle) {
if !handle.is_null() {
let _ = Box::from_raw(handle);
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_execute(handle: *mut AgentDbHandle, sql: *const c_char) -> i64 {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("agentdb_execute: null handle");
return -1;
}
};
let sql_str = match CStr::from_ptr(sql).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("agentdb_execute: invalid SQL string");
return -1;
}
};
match h.db.execute(sql_str) {
Ok(n) => n as i64,
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_query_json(
handle: *mut AgentDbHandle,
sql: *const c_char,
) -> *mut c_char {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("agentdb_query_json: null handle");
return std::ptr::null_mut();
}
};
let sql_str = match CStr::from_ptr(sql).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("agentdb_query_json: invalid SQL");
return std::ptr::null_mut();
}
};
match h.db.query_json(sql_str) {
Ok(rows) => {
let json = Value::Array(rows).to_string();
CString::new(json)
.map(|s| s.into_raw())
.unwrap_or(std::ptr::null_mut())
}
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_vector_upsert(
handle: *mut AgentDbHandle,
collection: *const c_char,
id: *const c_char,
vector: *const f32,
dim: usize,
metadata: *const c_char,
) -> i32 {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return -1;
}
};
let col_name = match CStr::from_ptr(collection).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid collection name");
return -1;
}
};
let id_str = match CStr::from_ptr(id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid id");
return -1;
}
};
let vec: Vec<f32> = std::slice::from_raw_parts(vector, dim).to_vec();
let meta: Option<Value> = if metadata.is_null() {
None
} else {
CStr::from_ptr(metadata)
.to_str()
.ok()
.and_then(|s| serde_json::from_str(s).ok())
};
let col = match h.db.vectors().collection(col_name, dim) {
Ok(c) => c,
Err(e) => {
set_last_error(e.to_string());
return -1;
}
};
match col.upsert(VectorEntry {
id: id_str.to_string(),
vector: vec,
metadata: meta,
}) {
Ok(()) => 0,
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_vector_search(
handle: *mut AgentDbHandle,
collection: *const c_char,
query: *const f32,
dim: usize,
top_k: usize,
filter_json: *const c_char,
) -> *mut c_char {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return std::ptr::null_mut();
}
};
let col_name = match CStr::from_ptr(collection).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid collection name");
return std::ptr::null_mut();
}
};
let q: Vec<f32> = std::slice::from_raw_parts(query, dim).to_vec();
let filter: Option<Value> = if filter_json.is_null() {
None
} else {
CStr::from_ptr(filter_json)
.to_str()
.ok()
.and_then(|s| serde_json::from_str(s).ok())
};
let col = match h.db.vectors().collection(col_name, dim) {
Ok(c) => c,
Err(e) => {
set_last_error(e.to_string());
return std::ptr::null_mut();
}
};
match col.search(
&q,
SearchOptions {
top_k,
metric: DistanceMetric::Cosine,
filter,
},
) {
Ok(results) => {
let json: Vec<Value> = results
.iter()
.map(
|r| serde_json::json!({ "id": r.id, "score": r.score, "metadata": r.metadata }),
)
.collect();
let s = Value::Array(json).to_string();
CString::new(s)
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_graph_add_node(
handle: *mut AgentDbHandle,
id: *const c_char,
kind: *const c_char,
data_json: *const c_char,
) -> i32 {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return -1;
}
};
let id_str = match CStr::from_ptr(id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid id");
return -1;
}
};
let kind_str = match CStr::from_ptr(kind).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid kind");
return -1;
}
};
let data: Option<Value> = if data_json.is_null() {
None
} else {
CStr::from_ptr(data_json)
.to_str()
.ok()
.and_then(|s| serde_json::from_str(s).ok())
};
match h.db.memory().add_node(id_str, kind_str, data) {
Ok(()) => 0,
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_graph_add_edge(
handle: *mut AgentDbHandle,
src: *const c_char,
dst: *const c_char,
relation: *const c_char,
weight: f64,
) -> i32 {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return -1;
}
};
let src_str = match CStr::from_ptr(src).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid src");
return -1;
}
};
let dst_str = match CStr::from_ptr(dst).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid dst");
return -1;
}
};
let relation_str = match CStr::from_ptr(relation).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid relation");
return -1;
}
};
match h
.db
.memory()
.add_edge(src_str, dst_str, relation_str, weight)
{
Ok(()) => 0,
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_graph_neighbors(
handle: *mut AgentDbHandle,
node_id: *const c_char,
max_depth: usize,
min_weight: f64,
) -> *mut c_char {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return std::ptr::null_mut();
}
};
let id_str = match CStr::from_ptr(node_id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid node_id");
return std::ptr::null_mut();
}
};
let opts = crate::memory::TraversalOptions {
relation: None,
max_depth,
min_weight: Some(min_weight),
};
match h.db.memory().neighbors(id_str, opts) {
Ok(results) => {
let json: Vec<Value> = results
.iter()
.map(|r| {
serde_json::json!({
"id": r.node.id,
"kind": r.node.kind,
"depth": r.depth,
"weight": r.weight,
"data": r.node.data
})
})
.collect();
let s = Value::Array(json).to_string();
CString::new(s)
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_fts_index(
handle: *mut AgentDbHandle,
collection: *const c_char,
vec_id: *const c_char,
collection_id: *const c_char,
text: *const c_char,
) -> i32 {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return -1;
}
};
let col = match CStr::from_ptr(collection).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid collection");
return -1;
}
};
let vid = match CStr::from_ptr(vec_id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid vec_id");
return -1;
}
};
let cid = match CStr::from_ptr(collection_id).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid collection_id");
return -1;
}
};
let txt = match CStr::from_ptr(text).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid text");
return -1;
}
};
match h.db.fts().index_text(col, vid, cid, txt) {
Ok(()) => 0,
Err(e) => {
set_last_error(e.to_string());
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_fts_search(
handle: *mut AgentDbHandle,
collection: *const c_char,
query: *const c_char,
top_k: usize,
) -> *mut c_char {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return std::ptr::null_mut();
}
};
let col = match CStr::from_ptr(collection).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid collection");
return std::ptr::null_mut();
}
};
let q = match CStr::from_ptr(query).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid query");
return std::ptr::null_mut();
}
};
match h.db.fts().search(col, q, top_k) {
Ok(results) => {
let json: Vec<Value> = results
.iter()
.map(|r| serde_json::json!({ "id": r.id, "snippet": r.snippet, "rank": r.rank }))
.collect();
let s = Value::Array(json).to_string();
CString::new(s)
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_hybrid_query(
handle: *mut AgentDbHandle,
anchor_node: *const c_char,
embedding: *const f32,
dim: usize,
collection: *const c_char,
graph_depth: usize,
top_k: usize,
alpha: f64,
) -> *mut c_char {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return std::ptr::null_mut();
}
};
let anchor = match CStr::from_ptr(anchor_node).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid anchor_node");
return std::ptr::null_mut();
}
};
let col = match CStr::from_ptr(collection).to_str() {
Ok(s) => s,
Err(_) => {
set_last_error("invalid collection");
return std::ptr::null_mut();
}
};
let emb: Vec<f32> = std::slice::from_raw_parts(embedding, dim).to_vec();
let q = HybridQuery {
anchor_node: anchor,
embedding: &emb,
collection: col,
graph_depth,
top_k,
alpha,
filter: None,
};
match h.db.hybrid_query(q) {
Ok(results) => {
let json: Vec<Value> = results
.iter()
.map(|r| {
serde_json::json!({
"id": r.id,
"rank_score": r.rank_score,
"vector_score": r.vector_score,
"graph_weight": r.graph_weight
})
})
.collect();
let s = Value::Array(json).to_string();
CString::new(s)
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn agentdb_stats(handle: *mut AgentDbHandle) -> *mut c_char {
clear_last_error();
let h = match handle.as_ref() {
Some(h) => h,
None => {
set_last_error("null handle");
return std::ptr::null_mut();
}
};
match h.db.stats() {
Ok(s) => {
let json = serde_json::json!({
"collections": s.collections,
"vectors": s.vectors,
"nodes": s.nodes,
"edges": s.edges
});
CString::new(json.to_string())
.map(|c| c.into_raw())
.unwrap_or(std::ptr::null_mut())
}
Err(e) => {
set_last_error(e.to_string());
std::ptr::null_mut()
}
}
}