diff --git a/elp/src/nlg.el b/elp/src/nlg.el index 77c543c..b33546c 100644 --- a/elp/src/nlg.el +++ b/elp/src/nlg.el @@ -1,22 +1,29 @@ // nlg.el - Public NLG API. // -// Ties together vocabulary, morphology, grammar, and realizer into a single -// entry point. Callers use `generate(semantic_form_json)` to produce English. +// Ties together vocabulary, morphology, grammar, realizer, and semantics into a +// single entry point. // -// SemanticForm JSON fields (all strings): -// intent - "assert" | "question" | "command" -// agent - subject (pronoun or noun phrase, optional for commands) -// predicate - verb base form -// patient - object noun phrase (optional) -// location - prepositional phrase e.g. "in the park" (optional) -// tense - "present" | "past" | "future" (default: "present") -// aspect - "simple" | "progressive" | "perfect" (default: "simple") +// Two entry points: +// +// generate(semantic_form_json) -> String +// Low-level JSON-based API (existing). SemanticForm JSON fields: +// intent - "assert" | "question" | "command" +// agent - subject (pronoun or noun phrase, optional for commands) +// predicate - verb base form +// patient - object noun phrase (optional) +// location - prepositional phrase e.g. "in the park" (optional) +// tense - "present" | "past" | "future" (default: "present") +// aspect - "simple" | "progressive" | "perfect" (default: "simple") +// +// generate_frame(frame: SemFrame) -> String +// High-level semantics API (new). frame is built with sem_frame() from +// semantics.el. Intents: "assert" | "query" | "describe" | "greet". // ── Include all sub-modules (concatenated at build time via nlg.el) ──────────────────────────── // In El, there is no import system for standalone programs, so the sub-modules // are inlined above this file in the build. When tests include nlg.el, they // first include vocabulary.el, morphology.el, grammar.el, realizer.el, -// then this file. +// semantics.el, then this file. // ── JSON helpers ────────────────────────────────────────────────────────────── @@ -25,7 +32,21 @@ fn sem_get(json: String, key: String) -> String { return val } -// ── Public API ──────────────────────────────────────────────────────────────── +// ── Public API: SemFrame ────────────────────────────────────────────────────── + +// Generate an English sentence from a SemFrame (built with sem_frame()). +// Uses the full semantics pipeline: sem_realize_full with the object field +// as the patient and sem_object as the predicate verb when intent is +// "assert" or "query", or the appropriate handler for "describe"/"greet". +// +// For assert/query, pass the verb separately via sem_realize_full directly +// when you need SVO (subject-verb-object) structure. This top-level entry +// point handles the common case where sem_object holds the predicate verb. +fn generate_frame(frame: [String]) -> String { + return sem_realize(frame) +} + +// ── Public API: JSON ────────────────────────────────────────────────────────── // Generate an English sentence from a semantic form JSON string. fn generate(semantic_form_json: String) -> String { diff --git a/elp/src/semantics.el b/elp/src/semantics.el new file mode 100644 index 0000000..a3c695d --- /dev/null +++ b/elp/src/semantics.el @@ -0,0 +1,284 @@ +// semantics.el - Semantic layer: SemFrame -> GramSpec (slot map for realizer). +// +// Bridges from intent/meaning representation to the grammar layer. +// A SemFrame is a slot map ([String] key-value list) with these keys: +// +// "intent" - "assert" | "query" | "describe" | "greet" +// "subject" - subject referent (pronoun or noun phrase) +// "object" - object referent (optional, "" if absent) +// "modifiers" - semicolon-separated modifier strings (e.g. "in the park;quickly") +// +// sem_to_spec converts a SemFrame into a realizer slot map ready for realize(). +// sem_realize is the end-to-end shortcut: frame -> realized English text. +// +// Depends on: grammar (slots_*), realizer (realize) + +// ── SemFrame constructors ───────────────────────────────────────────────────── + +// Build a SemFrame slot map with all four fields. +fn sem_frame(intent: String, subject: String, obj: String, modifiers: String) -> [String] { + let r: [String] = native_list_empty() + let r = native_list_append(r, "intent") + let r = native_list_append(r, intent) + let r = native_list_append(r, "subject") + let r = native_list_append(r, subject) + let r = native_list_append(r, "object") + let r = native_list_append(r, obj) + let r = native_list_append(r, "modifiers") + let r = native_list_append(r, modifiers) + return r +} + +// Convenience: frame with no object and no modifiers. +fn sem_frame_simple(intent: String, subject: String) -> [String] { + return sem_frame(intent, subject, "", "") +} + +// Convenience: frame with object but no modifiers. +fn sem_frame_obj(intent: String, subject: String, obj: String) -> [String] { + return sem_frame(intent, subject, obj, "") +} + +// ── SemFrame field accessors ────────────────────────────────────────────────── + +fn sem_intent(frame: [String]) -> String { + return slots_get(frame, "intent") +} + +fn sem_subject(frame: [String]) -> String { + return slots_get(frame, "subject") +} + +fn sem_object(frame: [String]) -> String { + return slots_get(frame, "object") +} + +fn sem_modifiers(frame: [String]) -> String { + return slots_get(frame, "modifiers") +} + +// ── Modifier helpers ────────────────────────────────────────────────────────── +// +// Modifiers are stored as a semicolon-delimited string. +// e.g. "in the park;quickly" +// sem_first_modifier extracts the first modifier (typically a location phrase). + +fn sem_first_modifier(mods: String) -> String { + let n: Int = str_len(mods) + if n == 0 { + return "" + } + // Scan to first ';' + let i: Int = 0 + let running: Bool = true + while running { + if i >= n { + let running = false + } else { + let c: String = str_slice(mods, i, i + 1) + if str_eq(c, ";") { + let running = false + } else { + let i = i + 1 + } + } + } + return str_slice(mods, 0, i) +} + +// ── Intent mapping ──────────────────────────────────────────────────────────── +// +// Maps semantic intent names to the slot values expected by realize(): +// "assert" -> intent="assert" declarative sentence +// "query" -> intent="question" yes/no question with do-support +// "describe" -> intent="assert" declarative; object holds predicate phrase +// "greet" -> handled specially "Hello, {subject}." + +fn sem_intent_to_realize(intent: String) -> String { + if str_eq(intent, "assert") { return "assert" } + if str_eq(intent, "query") { return "question" } + if str_eq(intent, "describe") { return "assert" } + if str_eq(intent, "greet") { return "greet" } + // Default: treat unknown as assertion + return "assert" +} + +// ── sem_to_spec: SemFrame -> realizer slot map ──────────────────────────────── +// +// Returns a [String] slot map suitable for passing directly to realize(). +// Default tense is "present", aspect is "simple". Callers that need +// a different tense/aspect should use slots_set on the returned map. + +fn sem_to_spec(frame: [String]) -> [String] { + let intent: String = sem_intent(frame) + let subject: String = sem_subject(frame) + let obj: String = sem_object(frame) + let mods: String = sem_modifiers(frame) + + let location: String = sem_first_modifier(mods) + + // ── Greet: no standard grammar slot, compose directly ───────────────── + // We still return a valid spec; sem_realize handles "greet" specially. + if str_eq(intent, "greet") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "greet") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + return spec + } + + // ── Describe: subject is topic, object is predicate noun/adj phrase ─── + // Realized as "Subject is Object." using the copula "be". + if str_eq(intent, "describe") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "assert") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "be") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + return spec + } + + // ── Assert / Query: standard subject-verb-object ─────────────────────── + let realize_intent: String = sem_intent_to_realize(intent) + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, realize_intent) + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, "") + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, "present") + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, "simple") + return spec +} + +// ── sem_to_spec_full: SemFrame -> realizer slot map with explicit verb ──────── +// +// For "assert" and "query" frames the object field carries the *noun phrase* +// patient, not the verb. Callers that have a separate verb should use this +// variant and supply it explicitly. +// +// verb - base form of the predicate verb +// tense - "present" | "past" | "future" +// aspect - "simple" | "progressive" | "perfect" + +fn sem_to_spec_full(frame: [String], verb: String, tense: String, aspect: String) -> [String] { + let intent: String = sem_intent(frame) + let subject: String = sem_subject(frame) + let obj: String = sem_object(frame) + let mods: String = sem_modifiers(frame) + + let location: String = sem_first_modifier(mods) + + if str_eq(intent, "greet") { + return sem_to_spec(frame) + } + + if str_eq(intent, "describe") { + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, "assert") + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, "be") + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, tense) + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, aspect) + return spec + } + + let realize_intent: String = sem_intent_to_realize(intent) + let spec: [String] = native_list_empty() + let spec = native_list_append(spec, "intent") + let spec = native_list_append(spec, realize_intent) + let spec = native_list_append(spec, "agent") + let spec = native_list_append(spec, subject) + let spec = native_list_append(spec, "predicate") + let spec = native_list_append(spec, verb) + let spec = native_list_append(spec, "patient") + let spec = native_list_append(spec, obj) + let spec = native_list_append(spec, "location") + let spec = native_list_append(spec, location) + let spec = native_list_append(spec, "tense") + let spec = native_list_append(spec, tense) + let spec = native_list_append(spec, "aspect") + let spec = native_list_append(spec, aspect) + return spec +} + +// ── Greet realization helper ────────────────────────────────────────────────── + +fn sem_realize_greet(subject: String) -> String { + if str_eq(subject, "") { + return "Hello." + } + return "Hello, " + subject + "." +} + +// ── sem_realize: SemFrame -> English text ───────────────────────────────────── +// +// End-to-end convenience: converts a SemFrame to a realized English string. +// For "assert" and "query" intents, the object field is treated as the +// predicate verb base form (intransitive use). For richer frames with both +// a verb and an object, use sem_realize_full. + +fn sem_realize(frame: [String]) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + let spec: [String] = sem_to_spec(frame) + return realize(spec) +} + +// ── sem_realize_full: SemFrame + explicit verb -> English text ──────────────── +// +// Like sem_realize but accepts a separate verb, tense, and aspect. +// Use this when the SemFrame object field carries a noun phrase patient +// and you want to specify the predicate verb independently. + +fn sem_realize_full(frame: [String], verb: String, tense: String, aspect: String) -> String { + let intent: String = sem_intent(frame) + + if str_eq(intent, "greet") { + return sem_realize_greet(sem_subject(frame)) + } + + let spec: [String] = sem_to_spec_full(frame, verb, tense, aspect) + return realize(spec) +} diff --git a/elp/tests/examples/sem-assert.el b/elp/tests/examples/sem-assert.el new file mode 100644 index 0000000..81eed8a --- /dev/null +++ b/elp/tests/examples/sem-assert.el @@ -0,0 +1,10 @@ +// sem-assert.el - Realize "She sleeps." via the semantics layer. +// +// SemFrame: intent="assert", subject="she", object="sleep" (verb), modifiers="" + +fn run_test() -> String { + let frame: [String] = sem_frame("assert", "she", "sleep", "") + return sem_realize(frame) +} + +println(run_test()) diff --git a/elp/tests/examples/sem-describe.el b/elp/tests/examples/sem-describe.el new file mode 100644 index 0000000..1874bb4 --- /dev/null +++ b/elp/tests/examples/sem-describe.el @@ -0,0 +1,10 @@ +// sem-describe.el - Realize "The cat is big." via the semantics layer. +// +// SemFrame: intent="describe", subject="the cat", object="big", modifiers="" + +fn run_test() -> String { + let frame: [String] = sem_frame("describe", "the cat", "big", "") + return sem_realize(frame) +} + +println(run_test()) diff --git a/elp/tests/examples/sem-greet.el b/elp/tests/examples/sem-greet.el new file mode 100644 index 0000000..657be60 --- /dev/null +++ b/elp/tests/examples/sem-greet.el @@ -0,0 +1,10 @@ +// sem-greet.el - Realize "Hello, Alice." via the semantics layer. +// +// SemFrame: intent="greet", subject="Alice", object="", modifiers="" + +fn run_test() -> String { + let frame: [String] = sem_frame("greet", "Alice", "", "") + return sem_realize(frame) +} + +println(run_test()) diff --git a/elp/tests/examples/sem-query.el b/elp/tests/examples/sem-query.el new file mode 100644 index 0000000..f65cfc1 --- /dev/null +++ b/elp/tests/examples/sem-query.el @@ -0,0 +1,10 @@ +// sem-query.el - Realize "Do you see?" via the semantics layer. +// +// SemFrame: intent="query", subject="you", object="see" (verb), modifiers="" + +fn run_test() -> String { + let frame: [String] = sem_frame("query", "you", "see", "") + return sem_realize(frame) +} + +println(run_test()) diff --git a/elp/tests/run.sh b/elp/tests/run.sh index 61ce4ca..a2f4d34 100755 --- a/elp/tests/run.sh +++ b/elp/tests/run.sh @@ -33,6 +33,7 @@ NLG_MODULES=( "${SRC_DIR}/vocabulary.el" "${SRC_DIR}/grammar.el" "${SRC_DIR}/realizer.el" + "${SRC_DIR}/semantics.el" "${SRC_DIR}/nlg.el" ) @@ -102,6 +103,10 @@ run_nlg_case "question" examples/question.el "Do you see the d run_nlg_case "command" examples/command.el "Go home." run_nlg_case "plural-noun" examples/plural-noun.el "cats|children|boxes|cities|fish|leaves" run_nlg_case "verb-conjugation" examples/verb-conjugation.el "ran|walked|is|am|sleeping" +run_nlg_case "sem-assert" examples/sem-assert.el "She sleeps." +run_nlg_case "sem-query" examples/sem-query.el "Do you see?" +run_nlg_case "sem-describe" examples/sem-describe.el "The cat is big." +run_nlg_case "sem-greet" examples/sem-greet.el "Hello, Alice." echo echo "${PASS} passed, ${FAIL} failed"