diff --git a/engram/dist/engram.c b/engram/dist/engram.c new file mode 100644 index 0000000..b5fe865 --- /dev/null +++ b/engram/dist/engram.c @@ -0,0 +1,376 @@ +#include +#include +#include "el_runtime.h" + +el_val_t parse_port(el_val_t bind); +el_val_t ok_json(void); +el_val_t err_json(el_val_t msg); +el_val_t strip_query(el_val_t path); +el_val_t query_param(el_val_t path, el_val_t key); +el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val); +el_val_t extract_id(el_val_t path, el_val_t prefix); +el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_search(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_save(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_load(el_val_t method, el_val_t path, el_val_t body); +el_val_t route_health(el_val_t method, el_val_t path, el_val_t body); +el_val_t check_auth_ok(el_val_t method, el_val_t body); +el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body); + +el_val_t bind_str; +el_val_t port; +el_val_t data_dir; +el_val_t snapshot_path; + +el_val_t parse_port(el_val_t bind) { + el_val_t colon = str_index_of(bind, EL_STR(":")); + if (colon < 0) { + return str_to_int(bind); + } + el_val_t after = str_slice(bind, (colon + 1), str_len(bind)); + return str_to_int(after); + return 0; +} + +el_val_t ok_json(void) { + return EL_STR("{\"ok\":true}"); + return 0; +} + +el_val_t err_json(el_val_t msg) { + return el_str_concat(el_str_concat(EL_STR("{\"error\":\""), msg), EL_STR("\"}")); + return 0; +} + +el_val_t strip_query(el_val_t path) { + el_val_t q = str_index_of(path, EL_STR("?")); + if (q < 0) { + return path; + } + return str_slice(path, 0, q); + return 0; +} + +el_val_t query_param(el_val_t path, el_val_t key) { + el_val_t q = str_index_of(path, EL_STR("?")); + if (q < 0) { + return EL_STR(""); + } + el_val_t qs = str_slice(path, (q + 1), str_len(path)); + el_val_t needle = el_str_concat(key, EL_STR("=")); + el_val_t pos = str_index_of(qs, needle); + if (pos < 0) { + return EL_STR(""); + } + el_val_t after = str_slice(qs, (pos + str_len(needle)), str_len(qs)); + el_val_t amp = str_index_of(after, EL_STR("&")); + if (amp < 0) { + return after; + } + return str_slice(after, 0, amp); + return 0; +} + +el_val_t query_int(el_val_t path, el_val_t key, el_val_t default_val) { + el_val_t v = query_param(path, key); + if (str_eq(v, EL_STR(""))) { + return default_val; + } + return str_to_int(v); + return 0; +} + +el_val_t extract_id(el_val_t path, el_val_t prefix) { + el_val_t clean = strip_query(path); + if (!str_starts_with(clean, prefix)) { + return EL_STR(""); + } + el_val_t after = str_slice(clean, str_len(prefix), str_len(clean)); + el_val_t slash = str_index_of(after, EL_STR("/")); + if (slash < 0) { + return after; + } + return str_slice(after, 0, slash); + return 0; +} + +el_val_t route_stats(el_val_t method, el_val_t path, el_val_t body) { + return engram_stats_json(); + return 0; +} + +el_val_t route_create_node(el_val_t method, el_val_t path, el_val_t body) { + el_val_t content = json_get_string(body, EL_STR("content")); + el_val_t node_type = json_get_string(body, EL_STR("node_type")); + if (str_eq(node_type, EL_STR(""))) { + node_type = EL_STR("Memory"); + } + el_val_t salience = json_get_float(body, EL_STR("salience")); + if (str_eq(salience, el_from_float(0.0))) { + salience = el_from_float(0.5); + } + el_val_t id = engram_node(content, node_type, salience); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), id), EL_STR("\",\"content\":\"")), content), EL_STR("\",\"node_type\":\"")), node_type), EL_STR("\"}")); + return 0; +} + +el_val_t route_get_node(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = extract_id(path, EL_STR("/api/nodes/")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing id")); + } + return engram_get_node_json(id); + return 0; +} + +el_val_t route_scan_nodes(el_val_t method, el_val_t path, el_val_t body) { + el_val_t limit = query_int(path, EL_STR("limit"), 50); + el_val_t offset = query_int(path, EL_STR("offset"), 0); + el_val_t nt = query_param(path, EL_STR("node_type")); + if (str_eq(nt, EL_STR(""))) { + return engram_scan_nodes_json(limit, offset); + } + return engram_scan_nodes_by_type_json(nt, limit, offset); + return 0; +} + +el_val_t route_scan_edges(el_val_t method, el_val_t path, el_val_t body) { + el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR")); + if (str_eq(dir, EL_STR(""))) { + dir = EL_STR("/tmp/engram"); + } + el_val_t snap_path = el_str_concat(dir, EL_STR("/snapshot.json")); + engram_save(snap_path); + el_val_t snap = fs_read(snap_path); + if (str_eq(snap, EL_STR(""))) { + return EL_STR("[]"); + } + el_val_t edges = json_get_raw(snap, EL_STR("edges")); + if (str_eq(edges, EL_STR(""))) { + return EL_STR("[]"); + } + return edges; + return 0; +} + +el_val_t route_search(el_val_t method, el_val_t path, el_val_t body) { + el_val_t q = EL_STR(""); + if (str_eq(method, EL_STR("GET"))) { + q = query_param(path, EL_STR("q")); + } else { + q = json_get_string(body, EL_STR("query")); + } + el_val_t limit = query_int(path, EL_STR("limit"), 20); + if (limit == 0) { + limit = json_get_int(body, EL_STR("limit")); + } + if (limit == 0) { + limit = 20; + } + return engram_search_json(q, limit); + return 0; +} + +el_val_t route_activate(el_val_t method, el_val_t path, el_val_t body) { + el_val_t q = EL_STR(""); + el_val_t depth = 3; + if (str_eq(method, EL_STR("GET"))) { + q = query_param(path, EL_STR("q")); + depth = query_int(path, EL_STR("depth"), 3); + } else { + q = json_get_string(body, EL_STR("query")); + el_val_t bd = json_get_int(body, EL_STR("depth")); + if (bd > 0) { + depth = bd; + } + } + return el_str_concat(el_str_concat(EL_STR("{\"results\":"), engram_activate_json(q, depth)), EL_STR("}")); + return 0; +} + +el_val_t route_create_edge(el_val_t method, el_val_t path, el_val_t body) { + el_val_t from_id = json_get_string(body, EL_STR("from_id")); + el_val_t to_id = json_get_string(body, EL_STR("to_id")); + el_val_t relation = json_get_string(body, EL_STR("relation")); + if (str_eq(relation, EL_STR(""))) { + relation = EL_STR("associates"); + } + el_val_t weight = json_get_float(body, EL_STR("weight")); + if (str_eq(weight, el_from_float(0.0))) { + weight = el_from_float(0.5); + } + engram_connect(from_id, to_id, weight, relation); + return el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"from_id\":\""), from_id), EL_STR("\",\"to_id\":\"")), to_id), EL_STR("\",\"relation\":\"")), relation), EL_STR("\"}")); + return 0; +} + +el_val_t route_neighbors(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = extract_id(path, EL_STR("/api/neighbors/")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing id")); + } + el_val_t depth = query_int(path, EL_STR("depth"), 1); + return engram_neighbors_json(id, depth, EL_STR("both")); + return 0; +} + +el_val_t route_strengthen(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = json_get_string(body, EL_STR("node_id")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing node_id")); + } + engram_strengthen(id); + return ok_json(); + return 0; +} + +el_val_t route_forget(el_val_t method, el_val_t path, el_val_t body) { + el_val_t id = extract_id(path, EL_STR("/api/nodes/")); + if (str_eq(id, EL_STR(""))) { + return err_json(EL_STR("missing id")); + } + engram_forget(id); + return ok_json(); + return 0; +} + +el_val_t route_save(el_val_t method, el_val_t path, el_val_t body) { + el_val_t p = json_get_string(body, EL_STR("path")); + if (str_eq(p, EL_STR(""))) { + el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR")); + if (str_eq(dir, EL_STR(""))) { + dir = EL_STR("/tmp/engram"); + } + p = el_str_concat(dir, EL_STR("/snapshot.json")); + } + engram_save(p); + return el_str_concat(el_str_concat(EL_STR("{\"ok\":true,\"path\":\""), p), EL_STR("\"}")); + return 0; +} + +el_val_t route_load(el_val_t method, el_val_t path, el_val_t body) { + el_val_t p = json_get_string(body, EL_STR("path")); + if (str_eq(p, EL_STR(""))) { + el_val_t dir = env(EL_STR("ENGRAM_DATA_DIR")); + if (str_eq(dir, EL_STR(""))) { + dir = EL_STR("/tmp/engram"); + } + p = el_str_concat(dir, EL_STR("/snapshot.json")); + } + engram_load(p); + return ok_json(); + return 0; +} + +el_val_t route_health(el_val_t method, el_val_t path, el_val_t body) { + return EL_STR("{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"); + return 0; +} + +el_val_t check_auth_ok(el_val_t method, el_val_t body) { + el_val_t key = env(EL_STR("ENGRAM_API_KEY")); + if (str_eq(key, EL_STR(""))) { + return 1; + } + if (str_eq(method, EL_STR("GET"))) { + return 1; + } + el_val_t provided = json_get_string(body, EL_STR("_auth")); + if (str_eq(provided, key)) { + return 1; + } + return 0; + return 0; +} + +el_val_t handle_request(el_val_t method, el_val_t path, el_val_t body) { + el_val_t clean = strip_query(path); + if (str_eq(method, EL_STR("GET"))) { + if (str_eq(clean, EL_STR("/health")) || str_eq(clean, EL_STR("/"))) { + return route_health(method, path, body); + } + } + if (!check_auth_ok(method, body)) { + return err_json(EL_STR("unauthorized")); + } + if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/stats")) || str_eq(clean, EL_STR("/stats")))) { + return route_stats(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")))) { + return route_create_node(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/nodes")) || str_eq(clean, EL_STR("/nodes")) || str_eq(clean, EL_STR("/nodes/list")) || str_eq(clean, EL_STR("/api/nodes/list")))) { + return route_scan_nodes(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) { + return route_scan_edges(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/nodes/"))) { + return route_get_node(method, path, body); + } + if (str_eq(method, EL_STR("DELETE")) && str_starts_with(clean, EL_STR("/api/nodes/"))) { + return route_forget(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/edges")) || str_eq(clean, EL_STR("/edges")))) { + return route_create_edge(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/neighbors/"))) { + return route_neighbors(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/activate")) || str_eq(clean, EL_STR("/activate")))) { + return route_activate(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/activate"))) { + return route_activate(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/search")) || str_eq(clean, EL_STR("/search")))) { + return route_search(method, path, body); + } + if (str_eq(method, EL_STR("GET")) && str_starts_with(clean, EL_STR("/api/search"))) { + return route_search(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/strengthen")) || str_eq(clean, EL_STR("/strengthen")))) { + return route_strengthen(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/save")) || str_eq(clean, EL_STR("/save")))) { + return route_save(method, path, body); + } + if (str_eq(method, EL_STR("POST")) && (str_eq(clean, EL_STR("/api/load")) || str_eq(clean, EL_STR("/load")))) { + return route_load(method, path, body); + } + return el_str_concat(el_str_concat(EL_STR("{\"error\":\"not found\",\"path\":\""), clean), EL_STR("\"}")); + return 0; +} + +int main(int argc, char** argv) { + el_runtime_init_args(argc, argv); + bind_str = env(EL_STR("ENGRAM_BIND")); + if (str_eq(bind_str, EL_STR(""))) { + bind_str = EL_STR(":8742"); + } + port = parse_port(bind_str); + data_dir = env(EL_STR("ENGRAM_DATA_DIR")); + if (str_eq(data_dir, EL_STR(""))) { + data_dir = EL_STR("/tmp/engram"); + } + snapshot_path = el_str_concat(data_dir, EL_STR("/snapshot.json")); + engram_load(snapshot_path); + println(EL_STR("[engram] runtime-native graph engine")); + println(el_str_concat(EL_STR("[engram] data_dir="), data_dir)); + println(el_str_concat(EL_STR("[engram] node_count="), int_to_str(engram_node_count()))); + println(el_str_concat(EL_STR("[engram] edge_count="), int_to_str(engram_edge_count()))); + println(el_str_concat(EL_STR("[engram] listening on "), int_to_str(port))); + http_set_handler(EL_STR("handle_request")); + http_serve(port, EL_STR("handle_request")); + return 0; +} + diff --git a/engram/src/server.el b/engram/src/server.el index e379690..ebaf34a 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -246,7 +246,7 @@ fn handle_request(method: String, path: String, body: String) -> String { if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) { return route_create_node(method, path, body) } - if str_eq(method, "GET") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) { + if str_eq(method, "GET") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes") || str_eq(clean, "/nodes/list") || str_eq(clean, "/api/nodes/list")) { return route_scan_nodes(method, path, body) } if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {