Fix ELP topic selection and replace broken agentic LLM call

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.
This commit is contained in:
Will Anderson
2026-05-03 11:50:18 -05:00
parent 2622bb04bd
commit af2ce3ddf3
2 changed files with 146 additions and 7 deletions
+128 -4
View File
@@ -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}"
}
+18 -3
View File
@@ -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 }