48ecd83421
- Fix wrong ELP import paths in soul.el, elp-input.el, studio.el (../foundation/elp/src → ../foundation/el/elp/src) - Add missing import "morphology.el" to all 29 language morphology modules - Recompile all affected dist/*.c with correct cross-module declarations - Add dist/elp-c-decls.h: C-level master forward declarations for ELP package (enables elb --force-include to resolve undeclared cross-module calls)
157 lines
7.6 KiB
EmacsLisp
157 lines
7.6 KiB
EmacsLisp
import "../foundation/el/elp/src/elp.el"
|
|
|
|
// elp-input.el — Convert free text → semantic frame for ELP input parsing.
|
|
//
|
|
// This is lightweight NLU: extracts predicate, arguments, and topic
|
|
// from a user message without calling an LLM. Covers the common cases:
|
|
// - "What is X?" → predicate=tell, arg=X
|
|
// - "Tell me about X" → predicate=tell, arg=X
|
|
// - "How do you feel about X?" → predicate=express, arg=X
|
|
// - "Remember X" → predicate=store, arg=X
|
|
// - "Who is X?" → predicate=identify, arg=X
|
|
// - "Why X?" → predicate=explain, arg=X
|
|
// - fallback → predicate=tell, arg=full message as topic
|
|
|
|
fn elp_extract_topic(msg: String) -> String {
|
|
// Strip common question prefixes to get the core topic
|
|
let m1: String = if str_starts_with(msg, "What is ") { str_slice(msg, 8, str_len(msg)) } else { msg }
|
|
let m2: String = if str_starts_with(m1, "What are ") { str_slice(m1, 9, str_len(m1)) } else { m1 }
|
|
let m3: String = if str_starts_with(m2, "Tell me about ") { str_slice(m2, 14, str_len(m2)) } else { m2 }
|
|
let m4: String = if str_starts_with(m3, "Who is ") { str_slice(m3, 7, str_len(m3)) } else { m3 }
|
|
let m5: String = if str_starts_with(m4, "Who are ") { str_slice(m4, 8, str_len(m4)) } else { m4 }
|
|
let m6: String = if str_starts_with(m5, "How do you ") { str_slice(m5, 11, str_len(m5)) } else { m5 }
|
|
let m7: String = if str_starts_with(m6, "Why ") { str_slice(m6, 4, str_len(m6)) } else { m6 }
|
|
let m8: String = if str_starts_with(m7, "Explain ") { str_slice(m7, 8, str_len(m7)) } else { m7 }
|
|
// Strip trailing punctuation
|
|
let last: Int = str_len(m8) - 1
|
|
let trail: String = str_slice(m8, last, str_len(m8))
|
|
let clean: String = if str_eq(trail, "?") || str_eq(trail, ".") || str_eq(trail, "!") {
|
|
str_slice(m8, 0, last)
|
|
} else {
|
|
m8
|
|
}
|
|
return clean
|
|
}
|
|
|
|
fn elp_detect_predicate(msg: String) -> String {
|
|
if str_starts_with(msg, "What is ") || str_starts_with(msg, "What are ") || str_starts_with(msg, "Tell me about ") {
|
|
return "tell"
|
|
}
|
|
if str_starts_with(msg, "Who is ") || str_starts_with(msg, "Who are ") {
|
|
return "identify"
|
|
}
|
|
if str_starts_with(msg, "Why ") || str_starts_with(msg, "Explain ") {
|
|
return "explain"
|
|
}
|
|
if str_starts_with(msg, "How do you feel") || str_starts_with(msg, "Do you ") {
|
|
return "express"
|
|
}
|
|
if str_starts_with(msg, "Remember ") || str_starts_with(msg, "Store ") {
|
|
return "store"
|
|
}
|
|
return "tell"
|
|
}
|
|
|
|
fn elp_parse(msg: String) -> String {
|
|
let predicate: String = elp_detect_predicate(msg)
|
|
let topic: String = elp_extract_topic(msg)
|
|
let safe_topic: String = str_replace(topic, "\"", "'")
|
|
return "{\"predicate\":\"" + predicate + "\",\"args\":[\"" + safe_topic + "\"],\"modifiers\":[],\"context\":{}}"
|
|
}
|
|
|
|
fn handle_elp_chat(body: String) -> String {
|
|
let message: String = json_get(body, "message")
|
|
if str_eq(message, "") {
|
|
return "{\"error\":\"message required\",\"response\":\"\"}"
|
|
}
|
|
|
|
let frame: String = elp_parse(message)
|
|
let predicate: String = elp_detect_predicate(message)
|
|
let topic: String = elp_extract_topic(message)
|
|
|
|
// ── Layer 1: Activate ────────────────────────────────────────────────────
|
|
// Graph walk from the extracted topic. Falls back to full message, then
|
|
// to a shallow scan if both activation paths return empty.
|
|
let from_topic: String = engram_activate_json(topic, 10)
|
|
let topic_ok: Bool = !str_eq(from_topic, "") && !str_eq(from_topic, "[]")
|
|
|
|
let candidates: String = if topic_ok {
|
|
from_topic
|
|
} else {
|
|
let from_msg: String = engram_activate_json(message, 10)
|
|
let msg_ok: Bool = !str_eq(from_msg, "") && !str_eq(from_msg, "[]")
|
|
if msg_ok { from_msg } else { engram_scan_nodes_json(5, 0) }
|
|
}
|
|
|
|
// ── Layer 2: Suppress / Filter ───────────────────────────────────────────
|
|
// Walk the candidates keeping nodes that have non-zero salience or
|
|
// importance. Always keep at least one (the top activation hit) even if
|
|
// all metrics are zero. Cap at 3 nodes — enough for a coherent reply.
|
|
let total: Int = json_array_len(candidates)
|
|
let fi: Int = 0
|
|
let kept_count: Int = 0
|
|
let kept_json: String = ""
|
|
while fi < total {
|
|
let n: String = json_array_get(candidates, fi)
|
|
let sal_str: String = json_get(n, "salience")
|
|
let imp_str: String = json_get(n, "importance")
|
|
let sal_ok: Bool = !str_eq(sal_str, "0") && !str_eq(sal_str, "0.0") && !str_eq(sal_str, "")
|
|
let imp_ok: Bool = !str_eq(imp_str, "0") && !str_eq(imp_str, "0.0") && !str_eq(imp_str, "")
|
|
let keep_it: Bool = sal_ok || imp_ok || kept_count == 0
|
|
if keep_it && kept_count < 3 {
|
|
let sep: String = if str_eq(kept_json, "") { "" } else { "," }
|
|
let kept_json = kept_json + sep + n
|
|
let kept_count = kept_count + 1
|
|
}
|
|
let fi = fi + 1
|
|
}
|
|
let frame_nodes: String = if str_eq(kept_json, "") { "[]" } else { "[" + kept_json + "]" }
|
|
|
|
// ── Reason ───────────────────────────────────────────────────────────────
|
|
// 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 }
|
|
}
|
|
let patient_safe: String = str_replace(str_replace(patient_raw, "\"", "'"), "\n", " ")
|
|
|
|
// ── Generate ─────────────────────────────────────────────────────────────
|
|
// Map ELP predicate → intent, then realize through the ELP grammar engine.
|
|
let intent_val: String = if str_eq(predicate, "store") { "command" } else { "assert" }
|
|
let gen_form: String = "{\"intent\":\"" + intent_val + "\""
|
|
+ ",\"agent\":\"I\""
|
|
+ ",\"predicate\":\"" + predicate + "\""
|
|
+ ",\"patient\":\"" + patient_safe + "\""
|
|
+ ",\"tense\":\"present\",\"aspect\":\"simple\",\"lang\":\"en\"}"
|
|
let realized: String = generate(gen_form)
|
|
|
|
let response: String = if str_eq(realized, "") {
|
|
if str_eq(patient_safe, "") {
|
|
"Nothing in the engram matched that query."
|
|
} else {
|
|
patient_safe
|
|
}
|
|
} else {
|
|
realized
|
|
}
|
|
let safe_resp: String = str_replace(str_replace(response, "\"", "'"), "\r", "")
|
|
return "{\"response\":\"" + safe_resp + "\",\"model\":\"elp-native\",\"frame\":" + frame + ",\"nodes\":" + frame_nodes + "}"
|
|
}
|