Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39acb55d4f | |||
| 343fcd20bc | |||
| d4609c7baa | |||
| 98603f5ae8 |
@@ -1202,6 +1202,86 @@ fn agentic_api_key() -> String {
|
|||||||
return env("NEURON_LLM_0_KEY")
|
return env("NEURON_LLM_0_KEY")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── OpenAI-compatible providers (Ollama / OpenAI / Grok / Gemini) ──────────────────────────────
|
||||||
|
// The brain speaks Anthropic's Messages format by default. When the active provider uses the
|
||||||
|
// OpenAI-compatible wire format (NEURON_LLM_0_FORMAT=openai) with a configured base URL
|
||||||
|
// (NEURON_LLM_0_URL, e.g. http://localhost:11434/v1 for local Ollama), basic chat turns are served
|
||||||
|
// here instead of the Anthropic agentic loop.
|
||||||
|
// v1 SCOPE: plain chat completion only — NO tools / agentic loop yet (that is a follow-up port).
|
||||||
|
// This block is ADDITIVE: the Anthropic path is untouched and stays the default.
|
||||||
|
|
||||||
|
fn llm_base_url() -> String {
|
||||||
|
return env("NEURON_LLM_0_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn llm_wire_format() -> String {
|
||||||
|
let f: String = env("NEURON_LLM_0_FORMAT")
|
||||||
|
if str_eq(f, "") {
|
||||||
|
return "anthropic"
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape a decoded string so it can be embedded back into a JSON string literal.
|
||||||
|
fn json_escape(s: String) -> String {
|
||||||
|
let a: String = str_replace(s, "\\", "\\\\")
|
||||||
|
let b: String = str_replace(a, "\"", "\\\"")
|
||||||
|
let c: String = str_replace(b, "\n", "\\n")
|
||||||
|
let d: String = str_replace(c, "\r", "\\r")
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic (non-agentic) chat completion against an OpenAI-compatible endpoint.
|
||||||
|
// [safe_sys] is already JSON-escaped; [messages_json] is the same JSON array the Anthropic path
|
||||||
|
// builds (e.g. [{"role":"user","content":"..."}]). Returns the soul's standard {"reply":"..."}.
|
||||||
|
fn openai_chat_complete(model: String, base_url: String, api_key: String, safe_sys: String, messages_json: String) -> String {
|
||||||
|
// Prepend the system prompt as an OpenAI "system" message, then the existing turn array.
|
||||||
|
let inner: String = if json_array_len(messages_json) > 0 {
|
||||||
|
str_slice(messages_json, 1, str_len(messages_json) - 1)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
let msgs: String = if str_eq(inner, "") {
|
||||||
|
"[{\"role\":\"system\",\"content\":\"" + safe_sys + "\"}]"
|
||||||
|
} else {
|
||||||
|
"[{\"role\":\"system\",\"content\":\"" + safe_sys + "\"}," + inner + "]"
|
||||||
|
}
|
||||||
|
let req_body: String = "{\"model\":\"" + model + "\""
|
||||||
|
+ ",\"max_tokens\":4096"
|
||||||
|
+ ",\"messages\":" + msgs
|
||||||
|
+ "}"
|
||||||
|
|
||||||
|
let h: Map = {}
|
||||||
|
map_set(h, "content-type", "application/json")
|
||||||
|
// Ollama needs no key; OpenAI / Grok / Gemini use a Bearer token.
|
||||||
|
if !str_eq(api_key, "") {
|
||||||
|
map_set(h, "Authorization", "Bearer " + api_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
let url: String = base_url + "/chat/completions"
|
||||||
|
let raw_resp: String = http_post_with_headers(url, req_body, h)
|
||||||
|
|
||||||
|
let is_error: Bool = str_starts_with(raw_resp, "{\"error\"") || str_contains(raw_resp, "\"error\":")
|
||||||
|
if is_error {
|
||||||
|
return "{\"error\":\"llm unavailable\",\"reply\":\"\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse OpenAI response shape: choices[0].message.content
|
||||||
|
let choices: String = json_get_raw(raw_resp, "choices")
|
||||||
|
let eff_choices: String = if str_eq(choices, "") {
|
||||||
|
"[]"
|
||||||
|
} else {
|
||||||
|
choices
|
||||||
|
}
|
||||||
|
if json_array_len(eff_choices) < 1 {
|
||||||
|
return "{\"error\":\"empty response\",\"reply\":\"\"}"
|
||||||
|
}
|
||||||
|
let first: String = json_array_get(eff_choices, 0)
|
||||||
|
let message: String = json_get_raw(first, "message")
|
||||||
|
let content: String = json_get(message, "content")
|
||||||
|
return "{\"reply\":\"" + json_escape(content) + "\",\"tools_used\":[]}"
|
||||||
|
}
|
||||||
|
|
||||||
fn agentic_tools_literal() -> String {
|
fn agentic_tools_literal() -> String {
|
||||||
return "[" +
|
return "[" +
|
||||||
"{\"name\":\"read_file\",\"description\":\"Read contents of a file from disk.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute file path\"}},\"required\":[\"path\"]}}," +
|
"{\"name\":\"read_file\",\"description\":\"Read contents of a file from disk.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\",\"description\":\"Absolute file path\"}},\"required\":[\"path\"]}}," +
|
||||||
@@ -1704,7 +1784,14 @@ fn handle_chat_agentic(body: String) -> String {
|
|||||||
|
|
||||||
// Use caller-supplied session_id if provided, otherwise generate a bridge id.
|
// Use caller-supplied session_id if provided, otherwise generate a bridge id.
|
||||||
let session_id: String = if str_eq(req_session, "") { next_bridge_id() } else { req_session }
|
let session_id: String = if str_eq(req_session, "") { next_bridge_id() } else { req_session }
|
||||||
let result: String = agentic_loop(session_id, model, safe_sys, tools_json, messages, h, "")
|
// Provider fork: OpenAI-compatible providers (Ollama/OpenAI/Grok/Gemini) take the plain-completion
|
||||||
|
// path (v1, no tools); everything else stays on the Anthropic agentic loop (the default).
|
||||||
|
let use_openai: Bool = !str_eq(llm_base_url(), "") && str_eq(llm_wire_format(), "openai")
|
||||||
|
let result: String = if use_openai {
|
||||||
|
openai_chat_complete(model, llm_base_url(), agentic_api_key(), safe_sys, messages)
|
||||||
|
} else {
|
||||||
|
agentic_loop(session_id, model, safe_sys, tools_json, messages, h, "")
|
||||||
|
}
|
||||||
|
|
||||||
// Persist the exchange to session/global history for thread continuity on next turn.
|
// Persist the exchange to session/global history for thread continuity on next turn.
|
||||||
// Only save when the loop completed (reply present), not when tool_pending.
|
// Only save when the loop completed (reply present), not when tool_pending.
|
||||||
|
|||||||
+30
@@ -2,6 +2,7 @@
|
|||||||
#include "el_runtime.h"
|
#include "el_runtime.h"
|
||||||
|
|
||||||
el_val_t add_punct(el_val_t s, el_val_t intent);
|
el_val_t add_punct(el_val_t s, el_val_t intent);
|
||||||
|
el_val_t add_to_seen(el_val_t seen, el_val_t node_id);
|
||||||
el_val_t aff_try_slot(el_val_t slot_json, el_val_t aff_7d_ts, el_val_t acc_key);
|
el_val_t aff_try_slot(el_val_t slot_json, el_val_t aff_7d_ts, el_val_t acc_key);
|
||||||
el_val_t agent_number(el_val_t agent);
|
el_val_t agent_number(el_val_t agent);
|
||||||
el_val_t agent_person(el_val_t agent);
|
el_val_t agent_person(el_val_t agent);
|
||||||
@@ -93,8 +94,10 @@ el_val_t api_err(el_val_t msg);
|
|||||||
el_val_t api_err_protected(el_val_t id);
|
el_val_t api_err_protected(el_val_t id);
|
||||||
el_val_t api_json_escape(el_val_t s);
|
el_val_t api_json_escape(el_val_t s);
|
||||||
el_val_t api_nonempty(el_val_t s);
|
el_val_t api_nonempty(el_val_t s);
|
||||||
|
el_val_t api_not_persisted(el_val_t id);
|
||||||
el_val_t api_ok(el_val_t extra);
|
el_val_t api_ok(el_val_t extra);
|
||||||
el_val_t api_or_empty(el_val_t s);
|
el_val_t api_or_empty(el_val_t s);
|
||||||
|
el_val_t api_persisted(el_val_t id);
|
||||||
el_val_t api_query_int(el_val_t path, el_val_t key, el_val_t default_val);
|
el_val_t api_query_int(el_val_t path, el_val_t key, el_val_t default_val);
|
||||||
el_val_t api_query_param(el_val_t path, el_val_t key);
|
el_val_t api_query_param(el_val_t path, el_val_t key);
|
||||||
el_val_t append_tool_log(el_val_t log, el_val_t name);
|
el_val_t append_tool_log(el_val_t log, el_val_t name);
|
||||||
@@ -138,6 +141,7 @@ el_val_t build_np(el_val_t referent, el_val_t slots);
|
|||||||
el_val_t build_pp(el_val_t loc);
|
el_val_t build_pp(el_val_t loc);
|
||||||
el_val_t build_rules(void);
|
el_val_t build_rules(void);
|
||||||
el_val_t build_system_prompt(el_val_t ctx);
|
el_val_t build_system_prompt(el_val_t ctx);
|
||||||
|
el_val_t build_system_prompt(el_val_t ctx, el_val_t chat_mode);
|
||||||
el_val_t build_vocab(void);
|
el_val_t build_vocab(void);
|
||||||
el_val_t build_vp_body(el_val_t slots);
|
el_val_t build_vp_body(el_val_t slots);
|
||||||
el_val_t build_vp_from_slots(el_val_t slots);
|
el_val_t build_vp_from_slots(el_val_t slots);
|
||||||
@@ -254,6 +258,19 @@ el_val_t en_verb_form(el_val_t base, el_val_t tense, el_val_t person, el_val_t n
|
|||||||
el_val_t en_verb_gerund(el_val_t base);
|
el_val_t en_verb_gerund(el_val_t base);
|
||||||
el_val_t en_verb_past(el_val_t base);
|
el_val_t en_verb_past(el_val_t base);
|
||||||
el_val_t engram_compile(el_val_t intent);
|
el_val_t engram_compile(el_val_t intent);
|
||||||
|
el_val_t engram_compile_multi(el_val_t topic);
|
||||||
|
el_val_t engram_compile_ranked(el_val_t nodes_json, el_val_t max_nodes);
|
||||||
|
el_val_t engram_dedup_nodes(el_val_t nodes_json);
|
||||||
|
el_val_t engram_detect_recall_intent(el_val_t message);
|
||||||
|
el_val_t engram_extract_entities(el_val_t message);
|
||||||
|
el_val_t engram_extract_ids(el_val_t nodes_json);
|
||||||
|
el_val_t engram_is_continuation(el_val_t message, el_val_t hist_len);
|
||||||
|
el_val_t engram_nodes_merge(el_val_t a, el_val_t b);
|
||||||
|
el_val_t engram_numeric_valid(el_val_t s);
|
||||||
|
el_val_t engram_render_node(el_val_t node_json);
|
||||||
|
el_val_t engram_render_nodes(el_val_t nodes_json);
|
||||||
|
el_val_t engram_score_node(el_val_t node_json);
|
||||||
|
el_val_t engram_split_topics(el_val_t message);
|
||||||
el_val_t enm_been_past(el_val_t slot);
|
el_val_t enm_been_past(el_val_t slot);
|
||||||
el_val_t enm_been_present(el_val_t slot);
|
el_val_t enm_been_present(el_val_t slot);
|
||||||
el_val_t enm_comen_past(el_val_t slot);
|
el_val_t enm_comen_past(el_val_t slot);
|
||||||
@@ -567,6 +584,9 @@ el_val_t handle_api_list_typed(el_val_t node_type, el_val_t path, el_val_t body)
|
|||||||
el_val_t handle_api_log_state_event(el_val_t body);
|
el_val_t handle_api_log_state_event(el_val_t body);
|
||||||
el_val_t handle_api_memory_delete(el_val_t body);
|
el_val_t handle_api_memory_delete(el_val_t body);
|
||||||
el_val_t handle_api_memory_update(el_val_t body);
|
el_val_t handle_api_memory_update(el_val_t body);
|
||||||
|
el_val_t handle_api_node_create(el_val_t body);
|
||||||
|
el_val_t handle_api_node_delete(el_val_t body);
|
||||||
|
el_val_t handle_api_node_update(el_val_t body);
|
||||||
el_val_t handle_api_promote_knowledge(el_val_t body);
|
el_val_t handle_api_promote_knowledge(el_val_t body);
|
||||||
el_val_t handle_api_recall(el_val_t method, el_val_t path, el_val_t body);
|
el_val_t handle_api_recall(el_val_t method, el_val_t path, el_val_t body);
|
||||||
el_val_t handle_api_remember(el_val_t body);
|
el_val_t handle_api_remember(el_val_t body);
|
||||||
@@ -648,6 +668,8 @@ el_val_t hi_verb_stem(el_val_t infinitive);
|
|||||||
el_val_t hi_verb_stem_clean(el_val_t infinitive);
|
el_val_t hi_verb_stem_clean(el_val_t infinitive);
|
||||||
el_val_t hist_append(el_val_t hist, el_val_t role, el_val_t content);
|
el_val_t hist_append(el_val_t hist, el_val_t role, el_val_t content);
|
||||||
el_val_t hist_trim(el_val_t hist);
|
el_val_t hist_trim(el_val_t hist);
|
||||||
|
el_val_t hist_trim_with_bell_guard(el_val_t hist);
|
||||||
|
el_val_t id_in_seen(el_val_t node_id, el_val_t seen);
|
||||||
el_val_t idle_count(void);
|
el_val_t idle_count(void);
|
||||||
el_val_t idle_inc(void);
|
el_val_t idle_inc(void);
|
||||||
el_val_t idle_reset(void);
|
el_val_t idle_reset(void);
|
||||||
@@ -836,6 +858,7 @@ el_val_t non_vera_present(el_val_t slot);
|
|||||||
el_val_t non_weak_past(el_val_t stem, el_val_t slot);
|
el_val_t non_weak_past(el_val_t stem, el_val_t slot);
|
||||||
el_val_t non_weak_present(el_val_t stem, el_val_t slot);
|
el_val_t non_weak_present(el_val_t stem, el_val_t slot);
|
||||||
el_val_t one_cycle(void);
|
el_val_t one_cycle(void);
|
||||||
|
el_val_t parse_float_x100(el_val_t s);
|
||||||
el_val_t parse_session_id_from_path(el_val_t path);
|
el_val_t parse_session_id_from_path(el_val_t path);
|
||||||
el_val_t parse_session_subpath(el_val_t path);
|
el_val_t parse_session_subpath(el_val_t path);
|
||||||
el_val_t path_within_root(el_val_t path, el_val_t root);
|
el_val_t path_within_root(el_val_t path, el_val_t root);
|
||||||
@@ -970,6 +993,7 @@ el_val_t safety_classify_hard_bell(el_val_t message);
|
|||||||
el_val_t safety_contact_path(void);
|
el_val_t safety_contact_path(void);
|
||||||
el_val_t safety_count_match(el_val_t text, el_val_t phrases_json);
|
el_val_t safety_count_match(el_val_t text, el_val_t phrases_json);
|
||||||
el_val_t safety_detect_bell_level(el_val_t message);
|
el_val_t safety_detect_bell_level(el_val_t message);
|
||||||
|
el_val_t safety_detect_positive_level(el_val_t message);
|
||||||
el_val_t safety_general_hard_phrases(void);
|
el_val_t safety_general_hard_phrases(void);
|
||||||
el_val_t safety_hard_directive(el_val_t hard_type);
|
el_val_t safety_hard_directive(el_val_t hard_type);
|
||||||
el_val_t safety_log_bell(el_val_t level, el_val_t reason, el_val_t input_summary);
|
el_val_t safety_log_bell(el_val_t level, el_val_t reason, el_val_t input_summary);
|
||||||
@@ -1007,13 +1031,19 @@ el_val_t sem_to_spec(el_val_t frame);
|
|||||||
el_val_t sem_to_spec_full(el_val_t frame, el_val_t verb, el_val_t tense, el_val_t aspect);
|
el_val_t sem_to_spec_full(el_val_t frame, el_val_t verb, el_val_t tense, el_val_t aspect);
|
||||||
el_val_t session_auto_title(el_val_t session_id, el_val_t first_message);
|
el_val_t session_auto_title(el_val_t session_id, el_val_t first_message);
|
||||||
el_val_t session_create(el_val_t body);
|
el_val_t session_create(el_val_t body);
|
||||||
|
el_val_t session_create_cleanup(el_val_t session_id);
|
||||||
el_val_t session_delete(el_val_t session_id);
|
el_val_t session_delete(el_val_t session_id);
|
||||||
|
el_val_t session_exists(el_val_t session_id);
|
||||||
el_val_t session_get(el_val_t session_id);
|
el_val_t session_get(el_val_t session_id);
|
||||||
el_val_t session_hist_load(el_val_t session_id);
|
el_val_t session_hist_load(el_val_t session_id);
|
||||||
el_val_t session_hist_save(el_val_t session_id, el_val_t hist);
|
el_val_t session_hist_save(el_val_t session_id, el_val_t hist);
|
||||||
el_val_t session_list(void);
|
el_val_t session_list(void);
|
||||||
el_val_t session_make_content(el_val_t id, el_val_t title, el_val_t created_at, el_val_t updated_at, el_val_t folder);
|
el_val_t session_make_content(el_val_t id, el_val_t title, el_val_t created_at, el_val_t updated_at, el_val_t folder);
|
||||||
|
el_val_t session_preload_bullets(el_val_t nodes, el_val_t max_bullets, el_val_t snip_len);
|
||||||
el_val_t session_search(el_val_t query);
|
el_val_t session_search(el_val_t query);
|
||||||
|
el_val_t session_summary_autogenerate(el_val_t hist);
|
||||||
|
el_val_t session_summary_write(el_val_t summary_text);
|
||||||
|
el_val_t session_summary_write_dated(el_val_t summary_text, el_val_t label);
|
||||||
el_val_t session_title_from_message(el_val_t message);
|
el_val_t session_title_from_message(el_val_t message);
|
||||||
el_val_t session_update_meta_timestamp(el_val_t session_id);
|
el_val_t session_update_meta_timestamp(el_val_t session_id);
|
||||||
el_val_t session_update_patch(el_val_t session_id, el_val_t body);
|
el_val_t session_update_patch(el_val_t session_id, el_val_t body);
|
||||||
|
|||||||
-25003
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
+259
-28681
File diff suppressed because one or more lines are too long
+219
-27617
File diff suppressed because one or more lines are too long
+27
-2
@@ -267,6 +267,27 @@ fn recall_or_list(query: String, limit: Int) -> String {
|
|||||||
return http_post_json(neuron_url() + "/recall", body)
|
return http_post_json(neuron_url() + "/recall", body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a real typed node via /api/neuron/node/create (handle_api_node_create) so it is a proper
|
||||||
|
// BacklogItem/Artifact/etc. — listable by type via /api/neuron/list/<type> — instead of a generic
|
||||||
|
// memory blob. Maps title->label, content/description->content, project/priority->tags.
|
||||||
|
fn create_node_typed(args: String, node_type: String, tier: String) -> String {
|
||||||
|
let content: String = pick_content(args)
|
||||||
|
if str_eq(content, "") {
|
||||||
|
return mcp_text_result("error: content/title is required for " + node_type)
|
||||||
|
}
|
||||||
|
let title: String = json_get_string(args, "title")
|
||||||
|
let label: String = if str_eq(title, "") { node_type } else { title }
|
||||||
|
let project: String = json_get_string(args, "project")
|
||||||
|
let priority: String = json_get_string(args, "priority")
|
||||||
|
let proj_tag: String = if str_eq(project, "") { "" } else { ",\"project:" + project + "\"" }
|
||||||
|
let prio_tag: String = if str_eq(priority, "") { "" } else { ",\"priority:" + priority + "\"" }
|
||||||
|
let tags: String = "[\"" + node_type + "\"" + proj_tag + prio_tag + "]"
|
||||||
|
let body: String = "{\"node_type\":\"" + node_type + "\",\"content\":\"" + json_escape(content)
|
||||||
|
+ "\",\"label\":\"" + json_escape(label) + "\",\"tier\":\"" + tier + "\",\"tags\":" + tags + "}"
|
||||||
|
let resp: String = http_post_json(neuron_url() + "/node/create", body)
|
||||||
|
return mcp_json_result(resp)
|
||||||
|
}
|
||||||
|
|
||||||
fn search_with_query(args: String, default_limit: Int) -> String {
|
fn search_with_query(args: String, default_limit: Int) -> String {
|
||||||
let query: String = json_get_string(args, "query")
|
let query: String = json_get_string(args, "query")
|
||||||
if str_eq(query, "") { let query = pick_content(args) }
|
if str_eq(query, "") { let query = pick_content(args) }
|
||||||
@@ -631,8 +652,12 @@ fn dispatch_tool_call(tool_name: String, args: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Backlog + work ──────────────────────────────────────────────────────
|
// ── Backlog + work ──────────────────────────────────────────────────────
|
||||||
if str_eq(tool_name, "planWork") { return create_typed_node(args, "BacklogItem", "0.65") }
|
// planWork: create a REAL typed BacklogItem via /api/neuron/node/create (the old path fell through
|
||||||
if str_eq(tool_name, "reviewBacklog") { return search_with_query(args, 50) }
|
// create_typed_node to a generic /memory write, dropping title/project/priority and never making a
|
||||||
|
// BacklogItem). reviewBacklog: LIST BacklogItem nodes (was a lexical /recall that never filtered by
|
||||||
|
// type). Both depend on the /api/neuron/list/<type> slice fix (neuron PR #58) to round-trip.
|
||||||
|
if str_eq(tool_name, "planWork") { return create_node_typed(args, "BacklogItem", "Working") }
|
||||||
|
if str_eq(tool_name, "reviewBacklog") { return list_typed("BacklogItem", 50, args) }
|
||||||
if str_eq(tool_name, "trackWork") { return evolve_by_supersede(args, "Memory") }
|
if str_eq(tool_name, "trackWork") { return evolve_by_supersede(args, "Memory") }
|
||||||
if str_eq(tool_name, "listWork") { return list_typed("WorkContext", 50, args) }
|
if str_eq(tool_name, "listWork") { return list_typed("WorkContext", 50, args) }
|
||||||
if str_eq(tool_name, "beginWork") { return create_typed_node(args, "Memory", "0.70") }
|
if str_eq(tool_name, "beginWork") { return create_typed_node(args, "Memory", "0.70") }
|
||||||
|
|||||||
Reference in New Issue
Block a user