From 1a8a16002ec4e66fa1eeaf4fdfd3281c8e86949e Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Thu, 14 May 2026 11:05:56 -0500 Subject: [PATCH] feat(engram): wire cosine similarity into Layer 2 activation scoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engram_cosine_sim() was defined and embeddings were computed per-node via nomic-embed-text on write, but the function was never called during activation scoring. The goal_bias computation used only lexical substring matching, ignoring all stored embedding vectors. This change adds engram_embed_query() to embed the query string at search time (5s timeout so Ollama latency never blocks activation), then blends cosine similarity into the working-memory bias with α=0.3: bias_final = goal_bias(lexical) * (1 + 0.3 * max(0, cosine_sim)) Nodes with high semantic similarity to the query but low lexical overlap now receive up to 30% bias boost into working memory promotion. Gracefully degrades to pure lexical when Ollama is unavailable or node has no embedding. --- engram/src/server.el | 7 +- lang/releases/v1.0.0-20260501/el_runtime.c | 86 ++++++++++++++++++++-- 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/engram/src/server.el b/engram/src/server.el index ec11720..bea33e0 100644 --- a/engram/src/server.el +++ b/engram/src/server.el @@ -493,8 +493,13 @@ fn route_neuron_config(method: String, path: String, body: String) -> String { "{\"key\":\"" + key + "\",\"value\":\"\"}" } -// route_neuron_state_events — log internal state event node +// route_neuron_state_events — GET lists ISEs, POST logs a new one fn route_neuron_state_events(method: String, path: String, body: String) -> String { + if str_eq(method, "GET") { + let limit_str: String = query_param(path, "limit") + let limit: Int = if str_eq(limit_str, "") { 50 } else { str_to_int(limit_str) } + return engram_scan_nodes_by_type_json("InternalStateEvent", limit, 0) + } let content: String = json_get_string(body, "content") if str_eq(content, "") { let content = body } let id: String = engram_node_full(content, "InternalStateEvent", "state-event", 0.3, 0.3, 1.0, "Working", "internal-state") diff --git a/lang/releases/v1.0.0-20260501/el_runtime.c b/lang/releases/v1.0.0-20260501/el_runtime.c index 721262c..d75661d 100644 --- a/lang/releases/v1.0.0-20260501/el_runtime.c +++ b/lang/releases/v1.0.0-20260501/el_runtime.c @@ -5955,11 +5955,12 @@ static void engram_persist_node(const char* data_dir, EngramNode* n); static void engram_persist_edge(const char* data_dir, EngramEdge* e); /* Binary persistence + embedding forward declarations. */ -static int engram_keys_init(void); -static int engram_write_binary(const char* path); -static int engram_load_binary(const char* path); -static void engram_embed_node(EngramNode* n); -static float engram_cosine_sim(const float* a, const float* b, uint32_t dim); +static int engram_keys_init(void); +static int engram_write_binary(const char* path); +static int engram_load_binary(const char* path); +static void engram_embed_node(EngramNode* n); +static uint32_t engram_embed_query(const char* text, float** vec_out); +static float engram_cosine_sim(const float* a, const float* b, uint32_t dim); static void engram_checkpoint(void); static void engram_emit_ise_internal(const char* content, const char* label); @@ -6870,9 +6871,17 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { double inh = best_bg[src] * e->weight; if (inh > inhibition[tgt]) inhibition[tgt] = inh; } + /* Embed the query string once for semantic similarity in Layer 2. + * Uses a 5s timeout so a slow/absent Ollama never blocks activation. + * query_emb is NULL and query_edim is 0 if embedding fails — all + * downstream cosine-sim paths guard on this and degrade to bias=1.0. */ + float* query_emb = NULL; + uint32_t query_edim = engram_embed_query(q, &query_emb); + /* Step B: compute working_memory_weight per candidate node. */ double* wm_weights = calloc((size_t)g->node_count, sizeof(double)); if (!wm_weights) { + free(query_emb); free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); free(inhibition); return out; } @@ -6883,6 +6892,19 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { double type_threshold = engram_type_threshold(n->node_type, n->tier); /* Goal bias weights the node's relevance to current intent. */ double bias = engram_goal_bias(n, q); + /* Cosine similarity boost: if both query and node have embeddings, + * blend semantic similarity into the bias with weight α=0.3. + * sim ∈ [-1, 1]; clamp to [0, 1] before blending. + * bias_final = bias * (1 + 0.3 * max(0, sim)) + * This boosts semantically close nodes even when lexical overlap is low. */ + if (query_emb && query_edim > 0 && + n->embedding && n->embedding_dim == query_edim) { + float sim = engram_cosine_sim(query_emb, n->embedding, query_edim); + if (sim > 0.0f) { + bias *= (1.0 + 0.3 * (double)sim); + if (bias > 2.0) bias = 2.0; + } + } /* Raw working memory score. */ double raw_wm = best_bg[i] * bias * n->confidence; /* Apply inhibitory suppression. Full inhibition → scale by factor. */ @@ -7032,6 +7054,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { Result* results = malloc((size_t)g->node_count * sizeof(Result)); int64_t rcount = 0; if (!results) { + free(query_emb); free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); free(inhibition); free(wm_weights); return out; } @@ -7076,6 +7099,7 @@ el_val_t engram_activate(el_val_t query, el_val_t depth) { (el_val_t)(results[i].wm > 0.0 ? 1 : 0)); out = el_list_append(out, entry); } + free(query_emb); free(best_bg); free(best_hops); free(reached); free(seeds); free(fr); free(inhibition); free(wm_weights); free(results); return out; @@ -7321,6 +7345,58 @@ static void engram_embed_node(EngramNode* n) { /* ── Engram: cosine similarity ───────────────────────────────────────────── */ +/* Embed an arbitrary text string into a float vector via Ollama. + * Returns the dimension (0 on failure). Caller must free *vec_out. */ +static uint32_t engram_embed_query(const char* text, float** vec_out) { + *vec_out = NULL; + if (!text || !*text) return 0; + size_t clen = strlen(text); + if (clen > 2048) clen = 2048; + char* body = malloc(clen * 6 + 128); + if (!body) return 0; + char* bp = body; + bp += sprintf(bp, "{\"model\":\"nomic-embed-text\",\"prompt\":\""); + const char* cp = text; + size_t written = 0; + while (*cp && written < clen) { + if (*cp == '"') { *bp++ = '\\'; *bp++ = '"'; } + else if (*cp == '\\') { *bp++ = '\\'; *bp++ = '\\'; } + else if (*cp == '\n') { *bp++ = '\\'; *bp++ = 'n'; } + else if (*cp == '\r') { *bp++ = '\\'; *bp++ = 'r'; } + else if (*cp == '\t') { *bp++ = '\\'; *bp++ = 't'; } + else { *bp++ = *cp; } + cp++; written++; + } + sprintf(bp, "\"}"); + CURL* curl = curl_easy_init(); + if (!curl) { free(body); return 0; } + char* resp = NULL; + struct curl_slist* hdrs = NULL; + hdrs = curl_slist_append(hdrs, "Content-Type: application/json"); + curl_easy_setopt(curl, CURLOPT_URL, "http://localhost:11434/api/embeddings"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, engram_embed_write_cb); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); /* short timeout — don't block activation */ + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + CURLcode rc = curl_easy_perform(curl); + curl_slist_free_all(hdrs); + curl_easy_cleanup(curl); + free(body); + if (rc != CURLE_OK || !resp) { free(resp); return 0; } + const char* ep = strstr(resp, "\"embedding\""); + if (!ep) { free(resp); return 0; } + ep += strlen("\"embedding\""); + while (*ep && *ep != '[') ep++; + float* vec = NULL; + uint32_t dim = engram_parse_float_array(ep, &vec); + free(resp); + if (dim == 0) return 0; + *vec_out = vec; + return dim; +} + static float engram_cosine_sim(const float* a, const float* b, uint32_t dim) { if (!a || !b || dim == 0) return 0.0f; double dot = 0.0, na = 0.0, nb = 0.0;