From af2ce3ddf36e757df8364343ddc20fd17f7ee4eb Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sun, 3 May 2026 11:50:18 -0500 Subject: [PATCH] Fix ELP topic selection and replace broken agentic LLM call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit elp-input.el: walk frame_nodes to pick the first node whose content contains the query topic (case-insensitive) rather than always taking index 0 — prevents the always-high-salience CGI architecture memory from hijacking every ELP response. chat.el: rewrite handle_chat_agentic to run the tool loop natively in El using http_post_with_headers + Map headers, bypassing the broken llm_call_agentic(model, system, message, tools) C binding that cast a JSON String as an ElList and never serialized tools. New impl supports up to 8 tool-use iterations with read_file, write_file, web_get, search_memory, and run_command dispatch. --- chat.el | 132 +++++++++++++++++++++++++++++++++++++++++++++++++-- elp-input.el | 21 ++++++-- 2 files changed, 146 insertions(+), 7 deletions(-) diff --git a/chat.el b/chat.el index 4b31510..1df794e 100644 --- a/chat.el +++ b/chat.el @@ -202,6 +202,54 @@ fn studio_tools_json() -> String { "]" } +fn agentic_api_key() -> String { + let k1: String = env("ANTHROPIC_API_KEY") + if !str_eq(k1, "") { + return k1 + } + return env("NEURON_LLM_0_KEY") +} + +fn agentic_tools_literal() -> String { + 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\":\"write_file\",\"description\":\"Write content to a file on disk.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"},\"content\":{\"type\":\"string\"}},\"required\":[\"path\",\"content\"]}}," + + "{\"name\":\"web_get\",\"description\":\"Fetch content from a URL.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"url\":{\"type\":\"string\"}},\"required\":[\"url\"]}}," + + "{\"name\":\"search_memory\",\"description\":\"Search engram memory for relevant nodes.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"]}}," + + "{\"name\":\"run_command\",\"description\":\"Run a shell command and capture output.\",\"input_schema\":{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\"}},\"required\":[\"command\"]}}" + + "]" +} + +fn dispatch_tool(tool_name: String, tool_input: String) -> String { + if str_eq(tool_name, "read_file") { + let path: String = json_get(tool_input, "path") + let content: String = fs_read(path) + return json_safe(content) + } + if str_eq(tool_name, "write_file") { + let path: String = json_get(tool_input, "path") + let content: String = json_get(tool_input, "content") + fs_write(path, content) + return "{\\\"ok\\\":true}" + } + if str_eq(tool_name, "web_get") { + let url: String = json_get(tool_input, "url") + let result: String = http_get(url) + return json_safe(result) + } + if str_eq(tool_name, "search_memory") { + let query: String = json_get(tool_input, "query") + let result: String = engram_search_json(query, 10) + return json_safe(result) + } + if str_eq(tool_name, "run_command") { + let cmd: String = json_get(tool_input, "command") + let result: String = exec_capture(cmd) + return json_safe(result) + } + return "unknown tool: " + tool_name +} + fn handle_chat_agentic(body: String) -> String { let message: String = json_get(body, "message") if str_eq(message, "") { @@ -215,14 +263,90 @@ fn handle_chat_agentic(body: String) -> String { let identity: String = state_get("soul_identity") let system: String = identity + " You have access to tools: read files, write files, browse the web, search your memory, run commands. Use them when they add genuine value. Be direct.\n\n" + ctx - let tools: String = studio_tools_json() - let text: String = llm_call_agentic(model, system, message, tools) + let api_key: String = agentic_api_key() + let tools_json: String = agentic_tools_literal() - if str_eq(text, "") { + // Build initial messages array + let safe_msg: String = json_safe(message) + let safe_sys: String = json_safe(system) + let messages: String = "[{\"role\":\"user\",\"content\":\"" + safe_msg + "\"}]" + + let api_url: String = "https://api.anthropic.com/v1/messages" + let h: Map = {} + map_set(h, "x-api-key", api_key) + map_set(h, "anthropic-version", "2023-06-01") + map_set(h, "content-type", "application/json") + + let final_text: String = "" + let iteration: Int = 0 + let keep_going: Bool = true + + while keep_going && iteration < 8 { + let req_body: String = "{\"model\":\"" + model + "\"" + + ",\"max_tokens\":4096" + + ",\"system\":\"" + safe_sys + "\"" + + ",\"tools\":" + tools_json + + ",\"messages\":" + messages + + "}" + + let raw_resp: String = http_post_with_headers(api_url, req_body, h) + + let is_error: Bool = str_starts_with(raw_resp, "{\"error\"") + || str_starts_with(raw_resp, "{\"type\":\"error\"") + || str_contains(raw_resp, "authentication_error") + if is_error { + return "{\"error\":\"llm unavailable\",\"reply\":\"\"}" + } + + let stop_reason: String = json_get(raw_resp, "stop_reason") + let content_arr: String = json_get(raw_resp, "content") + + // Collect text and tool_use blocks from content + let text_out: String = "" + let tool_results: String = "" + let has_tool_use: Bool = false + let ci: Int = 0 + let c_total: Int = json_array_len(content_arr) + while ci < c_total { + let block: String = json_array_get(content_arr, ci) + let block_type: String = json_get(block, "type") + if str_eq(block_type, "text") { + let block_text: String = json_get(block, "text") + let text_out = text_out + block_text + } + if str_eq(block_type, "tool_use") { + let has_tool_use = true + let tool_id: String = json_get(block, "id") + let tool_name: String = json_get(block, "name") + let tool_input: String = json_get(block, "input") + let tool_result: String = dispatch_tool(tool_name, tool_input) + let sep: String = if str_eq(tool_results, "") { "" } else { "," } + let tool_results = tool_results + sep + + "{\"type\":\"tool_result\",\"tool_use_id\":\"" + tool_id + "\",\"content\":\"" + tool_result + "\"}" + } + let ci = ci + 1 + } + + if str_eq(stop_reason, "tool_use") && has_tool_use { + // Append assistant turn with its content blocks, then tool results + let safe_content_arr: String = json_safe(content_arr) + let inner_msgs: String = str_slice(messages, 1, str_len(messages) - 1) + let messages = "[" + inner_msgs + + ",{\"role\":\"assistant\",\"content\":" + content_arr + "}" + + ",{\"role\":\"user\",\"content\":[" + tool_results + "]}" + + "]" + let iteration = iteration + 1 + } else { + let final_text = text_out + let keep_going = false + } + } + + if str_eq(final_text, "") { return "{\"error\":\"no response\",\"reply\":\"\"}" } - let safe_text: String = json_safe(text) + let safe_text: String = json_safe(final_text) return "{\"reply\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"agentic\":true}" } diff --git a/elp-input.el b/elp-input.el index 86cb069..dedb6aa 100644 --- a/elp-input.el +++ b/elp-input.el @@ -108,9 +108,24 @@ fn handle_elp_chat(body: String) -> String { let frame_nodes: String = if str_eq(kept_json, "") { "[]" } else { "[" + kept_json + "]" } // ── Reason ─────────────────────────────────────────────────────────────── - // Extract the patient from the top activated node's content. - // Trim to 200 chars so it fits cleanly in a generated sentence. - let top_node: String = json_array_get(frame_nodes, 0) + // Walk frame_nodes to find the first node whose content contains the topic. + // This prevents the always-high-salience CGI architecture node from + // dominating every response regardless of what was asked. + let fn_total: Int = json_array_len(frame_nodes) + let fn_i: Int = 0 + let topic_lower: String = str_to_lower(topic) + let found_node: String = "" + while fn_i < fn_total { + let candidate: String = json_array_get(frame_nodes, fn_i) + let cand_content: String = json_get(candidate, "content") + let cand_lower: String = str_to_lower(cand_content) + let matches: Bool = str_contains(cand_lower, topic_lower) + if matches && str_eq(found_node, "") { + let found_node = candidate + } + let fn_i = fn_i + 1 + } + let top_node: String = if str_eq(found_node, "") { json_array_get(frame_nodes, 0) } else { found_node } let top_raw: String = json_get(top_node, "content") let patient_raw: String = if str_eq(top_raw, "") { topic } else { if str_len(top_raw) > 200 { str_slice(top_raw, 0, 200) } else { top_raw }