From e8f5ce30b3a14dc9a4f1eb7fee3174f053ffaba6 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Thu, 19 Oct 2023 15:37:46 -0600 Subject: [PATCH 01/33] Moving "metta_space" and "metta_atom" test convenience functions from top-level Rust API into "test_utils" module --- lib/examples/sorted_list.rs | 19 ++++++------ lib/src/common/mod.rs | 13 +++++++-- lib/src/common/test_utils.rs | 17 +++++++++++ lib/src/metta/interpreter.rs | 2 ++ lib/src/metta/interpreter2.rs | 3 +- lib/src/metta/mod.rs | 51 --------------------------------- lib/src/metta/runner/mod.rs | 36 +++++++++++++++++++++-- lib/src/metta/runner/stdlib.rs | 2 ++ lib/src/metta/runner/stdlib2.rs | 3 ++ lib/src/metta/types.rs | 34 +++++++++++++--------- 10 files changed, 101 insertions(+), 79 deletions(-) create mode 100644 lib/src/common/test_utils.rs diff --git a/lib/examples/sorted_list.rs b/lib/examples/sorted_list.rs index 3649f419d..604cb571b 100644 --- a/lib/examples/sorted_list.rs +++ b/lib/examples/sorted_list.rs @@ -1,8 +1,8 @@ -use hyperon::metta::*; -use hyperon::metta::interpreter::*; +use hyperon::metta::runner::*; -fn main() { - let space = metta_space(" +fn main() -> Result<(), String> { + let metta = Metta::new(None); + metta.run_program_str(" (: List (-> $a Type)) (: Nil (List $a)) (: Cons (-> $a (List $a) (List $a))) @@ -15,11 +15,12 @@ fn main() { (= (insert $x (Cons $head $tail)) (if (< $x $head) (Cons $x (Cons $head $tail)) (Cons $head (insert $x $tail)))) - "); + ")?; + assert_eq!(metta.run_program_str("!(insert 1 Nil)")?[0], + vec![metta.parse_one_atom("(Cons 1 Nil)")?]); + assert_eq!(metta.run_program_str("(insert 3 (insert 2 (insert 1 Nil)))")?[0], + vec![metta.parse_one_atom("(Cons 1 (Cons 2 (Cons 3 Nil)))")?]); - assert_eq!(interpret(&space, &metta_atom("(insert 1 Nil)")), - Ok(vec![metta_atom("(Cons 1 Nil)")])); - assert_eq!(interpret(&space, &metta_atom("(insert 3 (insert 2 (insert 1 Nil)))")), - Ok(vec![metta_atom("(Cons 1 (Cons 2 (Cons 3 Nil)))")])); + Ok(()) } diff --git a/lib/src/common/mod.rs b/lib/src/common/mod.rs index b512582dc..066cea23f 100644 --- a/lib/src/common/mod.rs +++ b/lib/src/common/mod.rs @@ -14,11 +14,17 @@ mod arithmetics; pub use arithmetics::*; use crate::*; +use crate::metta::runner::*; +use crate::space::DynSpace; +use crate::space::grounding::GroundingSpace; +use crate::common::shared::Shared; +use crate::metta::text::Tokenizer; use std::cell::RefCell; use std::fmt::{Debug, Display}; use std::collections::HashMap; -use crate::metta::metta_atom; +#[cfg(test)] +pub(crate) mod test_utils; // TODO: move Operation and arithmetics under metta package as it uses metta_atom // Operation implements stateless operations as GroundedAtom. @@ -32,7 +38,10 @@ pub struct Operation { impl Grounded for &'static Operation { fn type_(&self) -> Atom { - metta_atom(self.typ) + //TODO: I think we want to be able to parse the type in the context of the current runner, rather than makeing a new one + //QUESTION: Why don't we parse the type once, and store the parsed type atom, rather than parsing it each time the accessor is called? + let metta = Metta::new_core(DynSpace::new(GroundingSpace::new()), Shared::new(Tokenizer::new()), None); + metta.parse_one_atom(self.typ).unwrap() } fn execute(&self, args: &[Atom]) -> Result, ExecError> { diff --git a/lib/src/common/test_utils.rs b/lib/src/common/test_utils.rs new file mode 100644 index 000000000..9365d0777 --- /dev/null +++ b/lib/src/common/test_utils.rs @@ -0,0 +1,17 @@ + +use crate::*; +use crate::metta::runner::{Metta, EnvBuilder}; +use crate::space::grounding::GroundingSpace; + +pub(crate) fn metta_space(text: &str) -> GroundingSpace { + let metta = Metta::new(Some(EnvBuilder::test_env())); + let atoms = metta.parse_all_atoms(text).unwrap(); + let mut space = GroundingSpace::new(); + atoms.into_iter().for_each(|atom| space.add(atom)); + space +} + +pub(crate) fn metta_atom(atom_str: &str) -> Atom { + let metta = Metta::new(Some(EnvBuilder::test_env())); + metta.parse_one_atom(atom_str).expect("Single atom is expected") +} diff --git a/lib/src/metta/interpreter.rs b/lib/src/metta/interpreter.rs index d6ff5658b..61a88e6fd 100644 --- a/lib/src/metta/interpreter.rs +++ b/lib/src/metta/interpreter.rs @@ -724,6 +724,8 @@ impl Debug for AlternativeInterpretationsPlan<'_, T> { #[cfg(test)] mod tests { use super::*; + use crate::common::*; + use crate::common::test_utils::*; #[test] fn test_match_all() { diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index 252605e3b..5daa012bc 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -402,6 +402,7 @@ fn cons(bindings: Bindings, head: Atom, tail: ExpressionAtom) -> Vec &str { } } -//QUESTION: These functions below seem to only be used in tests. If that's the case, should we -// move them into a test-only module so they aren't exposed as part of the public API? -//Alternatively rewrite the tests to use a runner? -// -//Also, a "Metta::parse_atoms() -> Vec" method -// might be useful to parse atoms with the context of a runner's tokenizer as a compliment to -// RunnerState::new_with_atoms - -// TODO: use stdlib to parse input text -pub fn metta_space(text: &str) -> GroundingSpace { - let tokenizer = common_tokenizer(); - let mut parser = SExprParser::new(text); - let mut space = GroundingSpace::new(); - loop { - let atom = parser.parse(&tokenizer).unwrap(); - if let Some(atom) = atom { - space.add(atom); - } else { - break; - } - } - space -} - -fn common_tokenizer() -> Tokenizer { - let mut tokenizer = Tokenizer::new(); - tokenizer.register_token(Regex::new(r"\d+").unwrap(), - |n| Atom::value(n.parse::().unwrap())); - tokenizer.register_token(Regex::new(r"true|false").unwrap(), - |b| Atom::value(b.parse::().unwrap())); - tokenizer.register_token(Regex::new(r"<").unwrap(), |_| Atom::gnd(LT)); - tokenizer -} - -// TODO: use stdlib to parse input text -pub fn metta_atom(atom: &str) -> Atom { - let tokenizer = common_tokenizer(); - let mut parser = SExprParser::new(atom); - let atom = parser.parse(&tokenizer).unwrap(); - if let Some(atom) = atom { - atom - } else { - panic!("Single atom is expected"); - } -} - - diff --git a/lib/src/metta/runner/mod.rs b/lib/src/metta/runner/mod.rs index 72b414d99..14420ee9a 100644 --- a/lib/src/metta/runner/mod.rs +++ b/lib/src/metta/runner/mod.rs @@ -241,11 +241,43 @@ impl Metta { self.0.settings.borrow().get(key.into()).map(|a| a.to_string()) } + /// Runs a MeTTa program expressed as a string of characters + pub fn run_program_str(&self, program: &str) -> Result>, String> { + let parser = SExprParser::new(program); + self.run(parser) + } + pub fn run<'p, 'a: 'p>(&'a self, parser: SExprParser<'p>) -> Result>, String> { let state = RunnerState::new_with_parser(self, parser); state.run_to_completion() } + /// Parses a string into a single Atom + pub fn parse_one_atom(&self, text: &str) -> Result { + let mut parser = SExprParser::new(text); + let atom = parser.parse(&*self.tokenizer().borrow())?; + if let Some(atom) = atom { + Ok(atom) + } else { + panic!("Expected a single atom"); + } + } + + /// Parses a string into a vector of Atoms + pub fn parse_all_atoms(&self, text: &str) -> Result, String> { + let mut parser = SExprParser::new(text); + let mut atoms = vec![]; + loop { + let atom = parser.parse(&*self.tokenizer().borrow())?; + if let Some(atom) = atom { + atoms.push(atom) + } else { + break; + } + } + Ok(atoms) + } + // TODO: this method is deprecated and should be removed after switching // to the minimal MeTTa pub fn evaluate_atom(&self, atom: Atom) -> Result, String> { @@ -493,7 +525,7 @@ mod tests { "; let metta = Metta::new(Some(EnvBuilder::test_env())); - metta.tokenizer().borrow_mut().register_token(Regex::new("error").unwrap(), + metta.tokenizer().borrow_mut().register_token_with_regex_str("error", |_| Atom::gnd(ErrorOp{})); let result = metta.run(SExprParser::new(program)); @@ -544,7 +576,7 @@ mod tests { "; let metta = Metta::new(Some(EnvBuilder::test_env())); - metta.tokenizer().borrow_mut().register_token(Regex::new("empty").unwrap(), + metta.tokenizer().borrow_mut().register_token_with_regex_str("empty", |_| Atom::gnd(ReturnAtomOp(expr!()))); let result = metta.run(SExprParser::new(program)); diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 3f741443b..b6137abd4 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1207,8 +1207,10 @@ pub static METTA_CODE: &'static str = " #[cfg(test)] mod tests { use super::*; + use crate::metta::text::*; use crate::metta::runner::{Metta, EnvBuilder}; use crate::metta::types::validate_atom; + use crate::common::test_utils::*; fn run_program(program: &str) -> Result>, String> { let metta = Metta::new(Some(EnvBuilder::test_env())); diff --git a/lib/src/metta/runner/stdlib2.rs b/lib/src/metta/runner/stdlib2.rs index 53db483c7..77545b3a0 100644 --- a/lib/src/metta/runner/stdlib2.rs +++ b/lib/src/metta/runner/stdlib2.rs @@ -411,8 +411,11 @@ pub static METTA_CODE: &'static str = include_str!("stdlib.metta"); #[cfg(test)] mod tests { use super::*; + use crate::metta::text::SExprParser; use crate::metta::runner::EnvBuilder; use crate::matcher::atoms_are_equivalent; + use crate::common::Operation; + use crate::common::test_utils::metta_space; use std::convert::TryFrom; diff --git a/lib/src/metta/types.rs b/lib/src/metta/types.rs index e1a5a7154..d18449051 100644 --- a/lib/src/metta/types.rs +++ b/lib/src/metta/types.rs @@ -156,16 +156,19 @@ fn get_args(expr: &ExpressionAtom) -> &[Atom] { /// /// ``` /// use hyperon::{Atom, expr, assert_eq_no_order}; -/// use hyperon::metta::{metta_space, ATOM_TYPE_UNDEFINED}; +/// use hyperon::metta::ATOM_TYPE_UNDEFINED; +/// use hyperon::metta::runner::*; /// use hyperon::metta::types::get_atom_types; /// -/// let space = metta_space(" +/// let metta = Metta::new(None); +/// metta.run_program_str(" /// (: f (-> A B)) /// (: a A) /// (: a B) /// (: b B) -/// "); +/// ").unwrap(); /// +/// let space = metta.space(); /// assert_eq_no_order!(get_atom_types(&space, &expr!(x)), vec![ATOM_TYPE_UNDEFINED]); /// assert_eq_no_order!(get_atom_types(&space, &expr!({1})), vec![expr!("i32")]); /// assert_eq_no_order!(get_atom_types(&space, &expr!("na")), vec![ATOM_TYPE_UNDEFINED]); @@ -386,12 +389,13 @@ fn get_matched_types(space: &dyn Space, atom: &Atom, typ: &Atom) -> Vec<(Atom, B /// /// ``` /// use hyperon::expr; -/// use hyperon::metta::metta_space; +/// use hyperon::metta::runner::*; /// use hyperon::metta::types::check_type; /// -/// let space = metta_space("(: a A) (: a B)"); +/// let metta = Metta::new(None); +/// metta.run_program_str("(: a A) (: a B)").unwrap(); /// -/// assert!(check_type(&space, &expr!("a"), &expr!("B"))); +/// assert!(check_type(&metta.space(), &expr!("a"), &expr!("B"))); /// ``` pub fn check_type(space: &dyn Space, atom: &Atom, typ: &Atom) -> bool { check_meta_type(atom, typ) || !get_matched_types(space, atom, typ).is_empty() @@ -405,11 +409,12 @@ pub fn check_type(space: &dyn Space, atom: &Atom, typ: &Atom) -> bool { /// /// ``` /// use hyperon::{expr, bind}; -/// use hyperon::metta::metta_space; +/// use hyperon::metta::runner::*; /// use hyperon::metta::types::get_type_bindings; /// -/// let space = metta_space("(: a (List A))"); -/// let types = get_type_bindings(&space, &expr!("a"), &expr!("List" t)); +/// let metta = Metta::new(None); +/// metta.run_program_str("(: a (List A))").unwrap(); +/// let types = get_type_bindings(&metta.space(), &expr!("a"), &expr!("List" t)); /// /// assert_eq!(types, vec![(expr!("List" "A"), bind!{ t: expr!("A") })]); /// ``` @@ -445,11 +450,13 @@ fn check_meta_type(atom: &Atom, typ: &Atom) -> bool { /// /// ``` /// use hyperon::expr; -/// use hyperon::metta::metta_space; +/// use hyperon::metta::runner::*; /// use hyperon::metta::types::validate_atom; /// -/// let space = metta_space("(: foo (-> A B)) (: a A) (: b B)"); +/// let metta = Metta::new(None); +/// metta.run_program_str("(: foo (-> A B)) (: a A) (: b B)").unwrap(); /// +/// let space = metta.space(); /// assert!(validate_atom(&space, &expr!("foo" "a"))); /// assert!(!validate_atom(&space, &expr!("foo" "b"))); /// ``` @@ -461,8 +468,7 @@ pub fn validate_atom(space: &dyn Space, atom: &Atom) -> bool { mod tests { use super::*; use crate::atom::matcher::atoms_are_equivalent; - use crate::metta::metta_space; - use crate::metta::metta_atom as atom; + use crate::common::test_utils::{metta_atom as atom, metta_space}; fn grammar_space() -> GroundingSpace { let mut space = GroundingSpace::new(); @@ -890,7 +896,7 @@ mod tests { (: p X) (: p P) "); - assert_eq!(get_atom_types(&space, &metta_atom("(= (foo) (bar p))")), + assert_eq!(get_atom_types(&space, &atom("(= (foo) (bar p))")), vec![expr!("Type")]); } From f5b84f5e05512636e74cee63556480b8332e061c Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 14 Apr 2023 14:28:12 +0300 Subject: [PATCH 02/33] Stop evaluation of expressions with variables as operations This is fast fix without pragma! because at the moment pragma! value is not reachable from the interpreter. More elegant fix could be implemented in minimal MeTTa. --- lib/src/metta/interpreter.rs | 34 ++++++++++++++++++++++++++-------- python/tests/test_examples.py | 18 +++++++++--------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/src/metta/interpreter.rs b/lib/src/metta/interpreter.rs index d6ff5658b..c9e89312d 100644 --- a/lib/src/metta/interpreter.rs +++ b/lib/src/metta/interpreter.rs @@ -310,6 +310,13 @@ fn is_grounded_op(expr: &ExpressionAtom) -> bool { } } +fn is_variable_op(expr: &ExpressionAtom) -> bool { + match expr.children().get(0) { + Some(Atom::Variable(_)) => true, + _ => false, + } +} + fn has_grounded_sub_expr(expr: &Atom) -> bool { return SubexprStream::from_expr(expr.clone(), TOP_DOWN_DEPTH_WALK) .any(|sub| if let Atom::Expression(sub) = sub { @@ -551,6 +558,8 @@ fn interpret_reducted_plan<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<' if let Atom::Expression(ref expr) = input.atom() { if is_grounded_op(expr) { Box::new(execute_plan(context, input)) + } else if is_variable_op(expr) { + Box::new(StepResult::ret(vec![input])) } else { Box::new(match_plan(context, input)) } @@ -743,16 +752,16 @@ mod tests { space.add(expr!("=" ("and" "True" "True") "True")); space.add(expr!("=" ("if" "True" then else) then)); space.add(expr!("=" ("if" "False" then else) else)); - space.add(expr!("=" ("Fritz" "croaks") "True")); - space.add(expr!("=" ("Fritz" "eats-flies") "True")); - space.add(expr!("=" ("Tweety" "chirps") "True")); - space.add(expr!("=" ("Tweety" "yellow") "True")); - space.add(expr!("=" ("Tweety" "eats-flies") "True")); - let expr = expr!("if" ("and" (x "croaks") (x "eats-flies")) - ("=" (x "frog") "True") "nop"); + space.add(expr!("=" ("croaks" "Fritz") "True")); + space.add(expr!("=" ("eats-flies" "Fritz") "True")); + space.add(expr!("=" ("chirps" "Tweety") "True")); + space.add(expr!("=" ("yellow" "Tweety") "True")); + space.add(expr!("=" ("eats-flies" "Tweety") "True")); + let expr = expr!("if" ("and" ("croaks" x) ("eats-flies" x)) + ("=" ("frog" x) "True") "nop"); assert_eq!(interpret(&space, &expr), - Ok(vec![expr!("=" ("Fritz" "frog") "True")])); + Ok(vec![expr!("=" ("frog" "Fritz") "True")])); } fn results_are_equivalent(actual: &Result, String>, @@ -1079,5 +1088,14 @@ mod tests { panic!("Non-empty result is expected"); } } + + #[test] + fn interpret_variable_does_not_match_operation_issue_242() { + let mut space = GroundingSpace::new(); + space.add(expr!("=" ("foo" x) ("foo result" x))); + space.add(expr!("=" ("bar" x) ("bar result" x))); + + assert_eq!(interpret(&space, &expr!(op "arg")), Ok(vec![expr!(op "arg")])); + } } diff --git a/python/tests/test_examples.py b/python/tests/test_examples.py index 58d907551..9a7c0110a 100644 --- a/python/tests/test_examples.py +++ b/python/tests/test_examples.py @@ -96,19 +96,19 @@ def test_frog_reasoning(self): metta = MeTTa(env_builder=Environment.test_env()) metta.run(''' - (= (Fritz croaks) True) - (= (Tweety chirps) True) - (= (Tweety yellow) True) - (= (Tweety eats_flies) True) - (= (Fritz eats_flies) True) + (= (croaks Fritz) True) + (= (chirps Tweety) True) + (= (yellow Tweety) True) + (= (eats_flies Tweety) True) + (= (eats_flies Fritz) True) ''') - fritz_frog = metta.run('!(if (and ($x croaks) ($x eats_flies)) (= ($x frog) True) nop)')[0] - self.assertEqual(metta.parse_all('(= (Fritz frog) True)'), fritz_frog) + fritz_frog = metta.run('!(if (and (croaks $x) (eats_flies $x)) (= (frog $x) True) nop)')[0] + self.assertEqual(metta.parse_all('(= (frog Fritz) True)'), fritz_frog) metta.space().add_atom(fritz_frog[0]) - self.assertEqualMettaRunnerResults([metta.parse_all('(= (Fritz green) True)')], - metta.run('!(if ($x frog) (= ($x green) True) nop)')) + self.assertEqualMettaRunnerResults([metta.parse_all('(= (green Fritz) True)')], + metta.run('!(if (frog $x) (= (green $x) True) nop)')) def test_infer_function_application_type(self): metta = MeTTa(env_builder=Environment.test_env()) From 91c5ad032b626f12e28c7225b4c4967d061743bd Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Mon, 30 Oct 2023 17:30:19 +0900 Subject: [PATCH 03/33] Creating default Environment config files only when explicitly directed. https://github.com/trueagi-io/hyperon-experimental/issues/475 --- c/src/metta.rs | 16 +++++++-- lib/src/metta/runner/environment.rs | 50 ++++++++++++++++++++++------- python/hyperon/runner.py | 8 +++-- python/hyperonpy.cpp | 1 + python/tests/test_environment.py | 2 +- repl/src/metta_shim.rs | 1 + repl/src/py_shim.py | 2 +- 7 files changed, 61 insertions(+), 19 deletions(-) diff --git a/c/src/metta.rs b/c/src/metta.rs index ad59ce2ac..738833c7b 100644 --- a/c/src/metta.rs +++ b/c/src/metta.rs @@ -891,7 +891,7 @@ pub extern "C" fn metta_nth_search_path(metta: *const metta_t, idx: usize, buf: let path = metta.search_paths().nth(idx); match path { Some(path) => write_into_buf(path.display(), buf, buf_len), - None => 0 + None => write_into_buf("", buf, buf_len) //Write just the terminator char, if there is room } } @@ -1118,7 +1118,7 @@ pub extern "C" fn metta_load_module(metta: *mut metta_t, name: *const c_char) { pub extern "C" fn environment_config_dir(buf: *mut c_char, buf_len: usize) -> usize { match Environment::common_env().config_dir() { Some(path) => write_into_buf(path.display(), buf, buf_len), - None => 0 + None => write_into_buf("", buf, buf_len) //Write just the terminator char, if there is room } } @@ -1239,6 +1239,18 @@ pub extern "C" fn env_builder_set_config_dir(builder: *mut env_builder_t, path: *builder_arg_ref = builder.into(); } +/// @brief Configures the environment to create the config dir if it doesn't already exist +/// @ingroup environment_group +/// @param[in] builder A pointer to the in-process environment builder state +/// +#[no_mangle] +pub extern "C" fn env_builder_create_config_dir(builder: *mut env_builder_t) { + let builder_arg_ref = unsafe{ &mut *builder }; + let builder = core::mem::replace(builder_arg_ref, env_builder_t::null()).into_inner(); + let builder = builder.create_config_dir(); + *builder_arg_ref = builder.into(); +} + /// @brief Configures the environment so that no config directory will be read nor created /// @ingroup environment_group /// @param[in] builder A pointer to the in-process environment builder state diff --git a/lib/src/metta/runner/environment.rs b/lib/src/metta/runner/environment.rs index fe765d5aa..948bf4ccf 100644 --- a/lib/src/metta/runner/environment.rs +++ b/lib/src/metta/runner/environment.rs @@ -78,6 +78,7 @@ impl Environment { pub struct EnvBuilder { env: Environment, no_cfg_dir: bool, + create_cfg_dir: bool, } impl EnvBuilder { @@ -97,6 +98,7 @@ impl EnvBuilder { Self { env: Environment::new(), no_cfg_dir: false, + create_cfg_dir: false, } } @@ -124,6 +126,17 @@ impl EnvBuilder { self } + /// Configures the environment to create a config directory with default config files, if no directory is found + /// + /// NOTE: If the config directory exists but some config files are missing, default files will not be created. + pub fn create_config_dir(mut self) -> Self { + self.create_cfg_dir = true; + if self.no_cfg_dir { + panic!("Fatal Error: create_config_dir is incompatible with set_no_config_dir"); + } + self + } + /// Configures the Environment not to load nor create any config files pub fn set_no_config_dir(mut self) -> Self { self.no_cfg_dir = true; @@ -169,12 +182,12 @@ impl EnvBuilder { /// /// NOTE: Creating owned Environments is usually not necessary. It is usually sufficient to use the [common_env] method. pub(crate) fn build(self) -> Environment { - let mut env = self.env; //Init the logger. This will have no effect if the logger has already been initialized let _ = env_logger::builder().is_test(env.is_test).try_init(); + //Construct the platform-specific config dir location, if an explicit location wasn't provided if !self.no_cfg_dir { if env.config_dir.is_none() { match ProjectDirs::from("io", "TrueAGI", "metta") { @@ -190,19 +203,17 @@ impl EnvBuilder { if let Some(config_dir) = &env.config_dir { - //Create the modules dir inside the config dir, if it doesn't already exist. - // This will create the cfg_dir iteslf in the process let modules_dir = config_dir.join("modules"); - std::fs::create_dir_all(&modules_dir).unwrap(); + let init_metta_path = config_dir.join("init.metta"); - //Push the "modules" dir, as the last place to search after the other paths that were specified - //TODO: the config.metta file will be able to append / modify the search paths, and can choose not to - // include the "modules" dir in the future. - env.extra_include_paths.push(modules_dir); + //Create the default config dir, if that part of our directive + if self.create_cfg_dir && !config_dir.exists() { - //Create the default init.metta file if it doesn't already exist - let init_metta_path = config_dir.join("init.metta"); - if !init_metta_path.exists() { + //Create the modules dir inside the config dir + // This will create the cfg_dir iteslf in the process + std::fs::create_dir_all(&modules_dir).unwrap(); + + //Create the default init.metta file let mut file = fs::OpenOptions::new() .create(true) .write(true) @@ -210,7 +221,22 @@ impl EnvBuilder { .expect(&format!("Error creating default init file at {init_metta_path:?}")); file.write_all(&DEFAULT_INIT_METTA).unwrap(); } - env.init_metta_path = Some(init_metta_path); + + //If the config_dir in the Environment still doesn't exist (and we couldn't create it), then set it to None + if !config_dir.exists() { + env.config_dir = None; + } + + //Push the "modules" dir, as the last place to search after the other paths that were specified + //TODO: the config.metta file should be able to append / modify the search paths, and can choose not to + // include the "modules" dir in the future. + if modules_dir.exists() { + env.extra_include_paths.push(modules_dir); + } + + if init_metta_path.exists() { + env.init_metta_path = Some(init_metta_path); + } } //TODO: This line below is a stop-gap to match old behavior diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py index 664063a26..b949501ff 100644 --- a/python/hyperon/runner.py +++ b/python/hyperon/runner.py @@ -196,22 +196,24 @@ def config_dir(): else: return None - def init_common_env(working_dir = None, config_dir = None, disable_config = False, is_test = False, include_paths = []): + def init_common_env(working_dir = None, config_dir = None, create_config = False, disable_config = False, is_test = False, include_paths = []): """Initialize the common environment with the supplied args""" - builder = Environment.custom_env(working_dir, config_dir, disable_config, is_test, include_paths) + builder = Environment.custom_env(working_dir, config_dir, create_config, disable_config, is_test, include_paths) return hp.env_builder_init_common_env(builder) def test_env(): """Returns an EnvBuilder object specifying a unit-test environment, that can be used to init a MeTTa runner""" return hp.env_builder_use_test_env() - def custom_env(working_dir = None, config_dir = None, disable_config = False, is_test = False, include_paths = []): + def custom_env(working_dir = None, config_dir = None, create_config = False, disable_config = False, is_test = False, include_paths = []): """Returns an EnvBuilder object that can be used to init a MeTTa runner, if you need multiple environments to coexist in the same process""" builder = hp.env_builder_start() if (working_dir is not None): hp.env_builder_set_working_dir(builder, working_dir) if (config_dir is not None): hp.env_builder_set_config_dir(builder, config_dir) + if (create_config): + hp.env_builder_create_config_dir(builder) if (disable_config): hp.env_builder_disable_config_dir(builder) if (is_test): diff --git a/python/hyperonpy.cpp b/python/hyperonpy.cpp index b8b6c70f1..7324677b8 100644 --- a/python/hyperonpy.cpp +++ b/python/hyperonpy.cpp @@ -850,6 +850,7 @@ PYBIND11_MODULE(hyperonpy, m) { m.def("env_builder_init_common_env", [](EnvBuilder builder) { return env_builder_init_common_env(builder.obj); }, "Finish initialization of the common environment"); m.def("env_builder_set_working_dir", [](EnvBuilder& builder, std::string path) { env_builder_set_working_dir(builder.ptr(), path.c_str()); }, "Sets the working dir in the environment"); m.def("env_builder_set_config_dir", [](EnvBuilder& builder, std::string path) { env_builder_set_config_dir(builder.ptr(), path.c_str()); }, "Sets the config dir in the environment"); + m.def("env_builder_create_config_dir", [](EnvBuilder& builder) { env_builder_create_config_dir(builder.ptr()); }, "Creates the config dir if it doesn't exist"); m.def("env_builder_disable_config_dir", [](EnvBuilder& builder) { env_builder_disable_config_dir(builder.ptr()); }, "Disables the config dir in the environment"); m.def("env_builder_set_is_test", [](EnvBuilder& builder, bool is_test) { env_builder_set_is_test(builder.ptr(), is_test); }, "Disables the config dir in the environment"); m.def("env_builder_add_include_path", [](EnvBuilder& builder, std::string path) { env_builder_add_include_path(builder.ptr(), path.c_str()); }, "Adds an include path to the environment"); diff --git a/python/tests/test_environment.py b/python/tests/test_environment.py index 3ef64be7a..b86cd4ec3 100644 --- a/python/tests/test_environment.py +++ b/python/tests/test_environment.py @@ -8,7 +8,7 @@ def __init__(self, methodName): super().__init__(methodName) def testEnvironment(self): - self.assertTrue(Environment.init_common_env(config_dir = "/tmp/test_dir")) + self.assertTrue(Environment.init_common_env(config_dir = "/tmp/test_dir", create_config = True)) self.assertEqual(Environment.config_dir(), "/tmp/test_dir") self.assertFalse(Environment.init_common_env(disable_config = True)) diff --git a/repl/src/metta_shim.rs b/repl/src/metta_shim.rs index 0d499ca28..5aa80e0e2 100644 --- a/repl/src/metta_shim.rs +++ b/repl/src/metta_shim.rs @@ -355,6 +355,7 @@ pub mod metta_interface_mod { pub fn init_common_env(working_dir: PathBuf, include_paths: Vec) -> Result { EnvBuilder::new() .set_working_dir(Some(&working_dir)) + .create_config_dir() .add_include_paths(include_paths) .init_common_env(); diff --git a/repl/src/py_shim.py b/repl/src/py_shim.py index a0ef111bb..bb4d0dd07 100644 --- a/repl/src/py_shim.py +++ b/repl/src/py_shim.py @@ -2,7 +2,7 @@ from hyperon import * def init_metta(working_dir, include_paths): - Environment.init_common_env(working_dir = working_dir, include_paths = include_paths) + Environment.init_common_env(working_dir = working_dir, create_config = True, include_paths = include_paths) return MeTTa() def load_metta_module(metta, mod_path): From c01cfcc08de36384ff6e7fc56f5c706043d79076 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Mon, 30 Oct 2023 15:00:02 +0300 Subject: [PATCH 04/33] Add variable_operation feature to turn on/off matching variable operations Feature is on by default but can be switched off. --- lib/Cargo.toml | 4 +++- lib/src/metta/interpreter.rs | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index ca281ebb0..a73510e52 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,5 +18,7 @@ path = "src/lib.rs" crate-type = ["lib"] [features] -#default = ["minimal"] +default = ["variable_operation"] +#default = ["variable_operation", "minimal"] minimal = [] +variable_operation = [] diff --git a/lib/src/metta/interpreter.rs b/lib/src/metta/interpreter.rs index c9e89312d..243819976 100644 --- a/lib/src/metta/interpreter.rs +++ b/lib/src/metta/interpreter.rs @@ -559,7 +559,11 @@ fn interpret_reducted_plan<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<' if is_grounded_op(expr) { Box::new(execute_plan(context, input)) } else if is_variable_op(expr) { - Box::new(StepResult::ret(vec![input])) + #[cfg(feature = "variable_operation")] + let result = Box::new(match_plan(context, input)); + #[cfg(not(feature = "variable_operation"))] + let result = Box::new(StepResult::ret(vec![input])); + result } else { Box::new(match_plan(context, input)) } @@ -1090,12 +1094,17 @@ mod tests { } #[test] - fn interpret_variable_does_not_match_operation_issue_242() { + fn interpret_match_variable_operation() { let mut space = GroundingSpace::new(); space.add(expr!("=" ("foo" x) ("foo result" x))); space.add(expr!("=" ("bar" x) ("bar result" x))); - assert_eq!(interpret(&space, &expr!(op "arg")), Ok(vec![expr!(op "arg")])); + let actual = interpret(&space, &expr!(op "arg")).unwrap(); + + #[cfg(feature = "variable_operation")] + assert_eq_no_order!(actual, vec![expr!("foo result" "arg"), expr!("bar result" "arg")]); + #[cfg(not(feature = "variable_operation"))] + assert_eq!(actual, vec![expr!(op "arg")]); } } From d26b2253616f932ea0475affebad260f928e871d Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 31 Oct 2023 12:36:41 +0900 Subject: [PATCH 05/33] Updating tests to remove dependency on Metta::run_program_str --- lib/examples/sorted_list.rs | 10 +++++----- lib/src/metta/types.rs | 14 +++++++++----- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/examples/sorted_list.rs b/lib/examples/sorted_list.rs index 604cb571b..aa4f66feb 100644 --- a/lib/examples/sorted_list.rs +++ b/lib/examples/sorted_list.rs @@ -1,8 +1,8 @@ -use hyperon::metta::runner::*; +use hyperon::metta::{runner::*, text::SExprParser}; fn main() -> Result<(), String> { let metta = Metta::new(None); - metta.run_program_str(" + metta.run(SExprParser::new(" (: List (-> $a Type)) (: Nil (List $a)) (: Cons (-> $a (List $a) (List $a))) @@ -15,11 +15,11 @@ fn main() -> Result<(), String> { (= (insert $x (Cons $head $tail)) (if (< $x $head) (Cons $x (Cons $head $tail)) (Cons $head (insert $x $tail)))) - ")?; + "))?; - assert_eq!(metta.run_program_str("!(insert 1 Nil)")?[0], + assert_eq!(metta.run(SExprParser::new("!(insert 1 Nil)"))?[0], vec![metta.parse_one_atom("(Cons 1 Nil)")?]); - assert_eq!(metta.run_program_str("(insert 3 (insert 2 (insert 1 Nil)))")?[0], + assert_eq!(metta.run(SExprParser::new("(insert 3 (insert 2 (insert 1 Nil)))"))?[0], vec![metta.parse_one_atom("(Cons 1 (Cons 2 (Cons 3 Nil)))")?]); Ok(()) diff --git a/lib/src/metta/types.rs b/lib/src/metta/types.rs index d18449051..4c32752df 100644 --- a/lib/src/metta/types.rs +++ b/lib/src/metta/types.rs @@ -158,15 +158,16 @@ fn get_args(expr: &ExpressionAtom) -> &[Atom] { /// use hyperon::{Atom, expr, assert_eq_no_order}; /// use hyperon::metta::ATOM_TYPE_UNDEFINED; /// use hyperon::metta::runner::*; +/// use hyperon::metta::text::SExprParser; /// use hyperon::metta::types::get_atom_types; /// /// let metta = Metta::new(None); -/// metta.run_program_str(" +/// metta.run(SExprParser::new(" /// (: f (-> A B)) /// (: a A) /// (: a B) /// (: b B) -/// ").unwrap(); +/// ")).unwrap(); /// /// let space = metta.space(); /// assert_eq_no_order!(get_atom_types(&space, &expr!(x)), vec![ATOM_TYPE_UNDEFINED]); @@ -390,10 +391,11 @@ fn get_matched_types(space: &dyn Space, atom: &Atom, typ: &Atom) -> Vec<(Atom, B /// ``` /// use hyperon::expr; /// use hyperon::metta::runner::*; +/// use hyperon::metta::text::SExprParser; /// use hyperon::metta::types::check_type; /// /// let metta = Metta::new(None); -/// metta.run_program_str("(: a A) (: a B)").unwrap(); +/// metta.run(SExprParser::new("(: a A) (: a B)")).unwrap(); /// /// assert!(check_type(&metta.space(), &expr!("a"), &expr!("B"))); /// ``` @@ -410,10 +412,11 @@ pub fn check_type(space: &dyn Space, atom: &Atom, typ: &Atom) -> bool { /// ``` /// use hyperon::{expr, bind}; /// use hyperon::metta::runner::*; +/// use hyperon::metta::text::SExprParser; /// use hyperon::metta::types::get_type_bindings; /// /// let metta = Metta::new(None); -/// metta.run_program_str("(: a (List A))").unwrap(); +/// metta.run(SExprParser::new("(: a (List A))")).unwrap(); /// let types = get_type_bindings(&metta.space(), &expr!("a"), &expr!("List" t)); /// /// assert_eq!(types, vec![(expr!("List" "A"), bind!{ t: expr!("A") })]); @@ -451,10 +454,11 @@ fn check_meta_type(atom: &Atom, typ: &Atom) -> bool { /// ``` /// use hyperon::expr; /// use hyperon::metta::runner::*; +/// use hyperon::metta::text::SExprParser; /// use hyperon::metta::types::validate_atom; /// /// let metta = Metta::new(None); -/// metta.run_program_str("(: foo (-> A B)) (: a A) (: b B)").unwrap(); +/// metta.run(SExprParser::new("(: foo (-> A B)) (: a A) (: b B)")).unwrap(); /// /// let space = metta.space(); /// assert!(validate_atom(&space, &expr!("foo" "a"))); From 28e464e3e807741a2309278411dcf49fd8fb84d9 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Tue, 31 Oct 2023 13:12:33 +0900 Subject: [PATCH 06/33] Removing Metta::parse_all_atoms and Metta::parse_one_atom, so Runner has absolutely no dependency on a particular syntax --- lib/examples/sorted_list.rs | 4 ++-- lib/src/common/mod.rs | 13 ++++--------- lib/src/common/test_utils.rs | 14 ++++++++++---- lib/src/metta/runner/mod.rs | 26 -------------------------- 4 files changed, 16 insertions(+), 41 deletions(-) diff --git a/lib/examples/sorted_list.rs b/lib/examples/sorted_list.rs index aa4f66feb..4c26b099b 100644 --- a/lib/examples/sorted_list.rs +++ b/lib/examples/sorted_list.rs @@ -18,9 +18,9 @@ fn main() -> Result<(), String> { "))?; assert_eq!(metta.run(SExprParser::new("!(insert 1 Nil)"))?[0], - vec![metta.parse_one_atom("(Cons 1 Nil)")?]); + vec![SExprParser::new("(Cons 1 Nil)").parse(&*metta.tokenizer().borrow())?.unwrap()]); assert_eq!(metta.run(SExprParser::new("(insert 3 (insert 2 (insert 1 Nil)))"))?[0], - vec![metta.parse_one_atom("(Cons 1 (Cons 2 (Cons 3 Nil)))")?]); + vec![SExprParser::new("(Cons 1 (Cons 2 (Cons 3 Nil)))").parse(&*metta.tokenizer().borrow())?.unwrap()]); Ok(()) } diff --git a/lib/src/common/mod.rs b/lib/src/common/mod.rs index 066cea23f..7d202697e 100644 --- a/lib/src/common/mod.rs +++ b/lib/src/common/mod.rs @@ -14,11 +14,7 @@ mod arithmetics; pub use arithmetics::*; use crate::*; -use crate::metta::runner::*; -use crate::space::DynSpace; -use crate::space::grounding::GroundingSpace; -use crate::common::shared::Shared; -use crate::metta::text::Tokenizer; +use crate::metta::text::{Tokenizer, SExprParser}; use std::cell::RefCell; use std::fmt::{Debug, Display}; use std::collections::HashMap; @@ -38,10 +34,9 @@ pub struct Operation { impl Grounded for &'static Operation { fn type_(&self) -> Atom { - //TODO: I think we want to be able to parse the type in the context of the current runner, rather than makeing a new one - //QUESTION: Why don't we parse the type once, and store the parsed type atom, rather than parsing it each time the accessor is called? - let metta = Metta::new_core(DynSpace::new(GroundingSpace::new()), Shared::new(Tokenizer::new()), None); - metta.parse_one_atom(self.typ).unwrap() + //TODO: Replace this parsing with a static Atom + let mut parser = SExprParser::new(self.typ); + parser.parse(&Tokenizer::new()).unwrap().unwrap() } fn execute(&self, args: &[Atom]) -> Result, ExecError> { diff --git a/lib/src/common/test_utils.rs b/lib/src/common/test_utils.rs index 9365d0777..ded97cb68 100644 --- a/lib/src/common/test_utils.rs +++ b/lib/src/common/test_utils.rs @@ -1,17 +1,23 @@ use crate::*; -use crate::metta::runner::{Metta, EnvBuilder}; +use crate::metta::runner::*; +use crate::metta::runner::EnvBuilder; +use crate::metta::text::SExprParser; use crate::space::grounding::GroundingSpace; pub(crate) fn metta_space(text: &str) -> GroundingSpace { let metta = Metta::new(Some(EnvBuilder::test_env())); - let atoms = metta.parse_all_atoms(text).unwrap(); let mut space = GroundingSpace::new(); - atoms.into_iter().for_each(|atom| space.add(atom)); + let mut parser = SExprParser::new(text); + while let Some(atom) = parser.parse(&metta.tokenizer().borrow()).unwrap() { + space.add(atom); + } space } pub(crate) fn metta_atom(atom_str: &str) -> Atom { let metta = Metta::new(Some(EnvBuilder::test_env())); - metta.parse_one_atom(atom_str).expect("Single atom is expected") + let mut parser = SExprParser::new(atom_str); + let atom = parser.parse(&metta.tokenizer().borrow()).unwrap().expect("Single atom is expected"); + atom } diff --git a/lib/src/metta/runner/mod.rs b/lib/src/metta/runner/mod.rs index fdcc1d870..f4ec0aeb0 100644 --- a/lib/src/metta/runner/mod.rs +++ b/lib/src/metta/runner/mod.rs @@ -250,32 +250,6 @@ impl Metta { state.run_to_completion() } - /// Parses a string into a single Atom - pub fn parse_one_atom(&self, text: &str) -> Result { - let mut parser = SExprParser::new(text); - let atom = parser.parse(&*self.tokenizer().borrow())?; - if let Some(atom) = atom { - Ok(atom) - } else { - panic!("Expected a single atom"); - } - } - - /// Parses a string into a vector of Atoms - pub fn parse_all_atoms(&self, text: &str) -> Result, String> { - let mut parser = SExprParser::new(text); - let mut atoms = vec![]; - loop { - let atom = parser.parse(&*self.tokenizer().borrow())?; - if let Some(atom) = atom { - atoms.push(atom) - } else { - break; - } - } - Ok(atoms) - } - // TODO: this method is deprecated and should be removed after switching // to the minimal MeTTa pub fn evaluate_atom(&self, atom: Atom) -> Result, String> { From ec3641c415d8027de00c8844998bb5c95951f754 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Tue, 31 Oct 2023 13:26:00 +0300 Subject: [PATCH 07/33] Use explicit atom instantiation in sorted_list example --- lib/examples/sorted_list.rs | 16 +++++------ lib/src/metta/runner/arithmetics.rs | 43 +++++++++++++++++++++-------- lib/src/metta/runner/mod.rs | 2 +- lib/src/metta/runner/stdlib.rs | 10 +++++++ 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/lib/examples/sorted_list.rs b/lib/examples/sorted_list.rs index 4c26b099b..89a35c8aa 100644 --- a/lib/examples/sorted_list.rs +++ b/lib/examples/sorted_list.rs @@ -1,4 +1,7 @@ -use hyperon::metta::{runner::*, text::SExprParser}; +use hyperon::*; +use hyperon::metta::runner::*; +use hyperon::metta::runner::arithmetics::*; +use hyperon::metta::text::SExprParser; fn main() -> Result<(), String> { let metta = Metta::new(None); @@ -7,10 +10,7 @@ fn main() -> Result<(), String> { (: Nil (List $a)) (: Cons (-> $a (List $a) (List $a))) - (: if (-> bool Any Any) Any) - (= (if true $then $else) $then) - (= (if false $then $else) $else) - + (: insert (-> $a (List $a) (List $a))) (= (insert $x Nil) (Cons $x Nil)) (= (insert $x (Cons $head $tail)) (if (< $x $head) (Cons $x (Cons $head $tail)) @@ -18,9 +18,9 @@ fn main() -> Result<(), String> { "))?; assert_eq!(metta.run(SExprParser::new("!(insert 1 Nil)"))?[0], - vec![SExprParser::new("(Cons 1 Nil)").parse(&*metta.tokenizer().borrow())?.unwrap()]); - assert_eq!(metta.run(SExprParser::new("(insert 3 (insert 2 (insert 1 Nil)))"))?[0], - vec![SExprParser::new("(Cons 1 (Cons 2 (Cons 3 Nil)))").parse(&*metta.tokenizer().borrow())?.unwrap()]); + vec![expr!("Cons" {Number::Integer(1)} "Nil")]); + assert_eq!(metta.run(SExprParser::new("!(insert 3 (insert 2 (insert 1 Nil)))"))?[0], + vec![expr!("Cons" {Number::Integer(1)} ("Cons" {Number::Integer(2)} ("Cons" {Number::Integer(3)} "Nil")))]); Ok(()) } diff --git a/lib/src/metta/runner/arithmetics.rs b/lib/src/metta/runner/arithmetics.rs index c2d659b42..54ddb7ad3 100644 --- a/lib/src/metta/runner/arithmetics.rs +++ b/lib/src/metta/runner/arithmetics.rs @@ -13,6 +13,22 @@ pub enum Number { Float(f64), } +trait IntoNumber { + fn into_num(self) -> Number; +} + +impl IntoNumber for i64 { + fn into_num(self) -> Number { + Number::Integer(self) + } +} + +impl IntoNumber for f64 { + fn into_num(self) -> Number { + Number::Float(self) + } +} + impl Number { pub fn from_int_str(num: &str) -> Self { let n = num.parse::().expect("Could not parse integer"); @@ -85,7 +101,7 @@ impl Grounded for Bool { } macro_rules! def_binary_number_op { - ($name:ident, $op:tt) => { + ($name:ident, $op:tt, $r:ident, $cast:expr) => { #[derive(Clone, PartialEq, Debug)] pub struct $name{} @@ -97,7 +113,7 @@ macro_rules! def_binary_number_op { impl Grounded for $name { fn type_(&self) -> Atom { - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_NUMBER, ATOM_TYPE_NUMBER, $r]) } fn execute(&self, args: &[Atom]) -> Result, ExecError> { @@ -106,10 +122,10 @@ macro_rules! def_binary_number_op { let b = args.get(1).ok_or_else(arg_error)?.as_gnd::().ok_or_else(arg_error)?; let res = match (a, b) { - (Number::Integer(a), Number::Integer(b)) => Number::Integer(a $op b), - (Number::Integer(a), Number::Float(b)) => Number::Float((*a as f64) $op b), - (Number::Float(a), Number::Integer(b)) => Number::Float(a $op (*b as f64)), - (Number::Float(a), Number::Float(b)) => Number::Float(a $op b), + (Number::Integer(a), Number::Integer(b)) => $cast(a $op b), + (Number::Integer(a), Number::Float(b)) => $cast((*a as f64) $op *b), + (Number::Float(a), Number::Integer(b)) => $cast(*a $op (*b as f64)), + (Number::Float(a), Number::Float(b)) => $cast(a $op b), }; Ok(vec![Atom::gnd(res)]) @@ -122,11 +138,16 @@ macro_rules! def_binary_number_op { } } -def_binary_number_op!(SumOp, +); -def_binary_number_op!(SubOp, -); -def_binary_number_op!(MulOp, *); -def_binary_number_op!(DivOp, /); -def_binary_number_op!(ModOp, %); +def_binary_number_op!(SumOp, +, ATOM_TYPE_NUMBER, IntoNumber::into_num); +def_binary_number_op!(SubOp, -, ATOM_TYPE_NUMBER, IntoNumber::into_num); +def_binary_number_op!(MulOp, *, ATOM_TYPE_NUMBER, IntoNumber::into_num); +def_binary_number_op!(DivOp, /, ATOM_TYPE_NUMBER, IntoNumber::into_num); +def_binary_number_op!(ModOp, %, ATOM_TYPE_NUMBER, IntoNumber::into_num); +def_binary_number_op!(LessOp, <, ATOM_TYPE_BOOL, Bool); +def_binary_number_op!(GreaterOp, >, ATOM_TYPE_BOOL, Bool); +def_binary_number_op!(LessEqOp, <=, ATOM_TYPE_BOOL, Bool); +def_binary_number_op!(GreaterEqOp, >=, ATOM_TYPE_BOOL, Bool); +def_binary_number_op!(EqualOp, ==, ATOM_TYPE_BOOL, Bool); #[cfg(test)] mod tests { diff --git a/lib/src/metta/runner/mod.rs b/lib/src/metta/runner/mod.rs index f4ec0aeb0..0ad3b24e5 100644 --- a/lib/src/metta/runner/mod.rs +++ b/lib/src/metta/runner/mod.rs @@ -27,7 +27,7 @@ use super::interpreter2::{interpret, interpret_init, interpret_step, Interpreter #[cfg(feature = "minimal")] use stdlib2::*; -mod arithmetics; +pub mod arithmetics; const EXEC_SYMBOL : Atom = sym!("!"); diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index d4e6ad9f2..699b8ea65 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1182,6 +1182,16 @@ pub fn register_rust_tokens(metta: &Metta) { tref.register_token(regex(r"/"), move |_| { div_op.clone() }); let mod_op = Atom::gnd(ModOp{}); tref.register_token(regex(r"%"), move |_| { mod_op.clone() }); + let lt_op = Atom::gnd(LessOp{}); + tref.register_token(regex(r"<"), move |_| { lt_op.clone() }); + let gt_op = Atom::gnd(GreaterOp{}); + tref.register_token(regex(r">"), move |_| { gt_op.clone() }); + let le_op = Atom::gnd(LessEqOp{}); + tref.register_token(regex(r"<="), move |_| { le_op.clone() }); + let ge_op = Atom::gnd(GreaterEqOp{}); + tref.register_token(regex(r">="), move |_| { ge_op.clone() }); + let eq_op = Atom::gnd(EqualOp{}); + tref.register_token(regex(r"=="), move |_| { eq_op.clone() }); metta.tokenizer().borrow_mut().move_front(&mut rust_tokens); } From f901ae28458170a14582647366c95dc115b16775 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Tue, 31 Oct 2023 16:55:48 +0300 Subject: [PATCH 08/33] Guidance-style llm-gate usage --- python/sandbox/neurospace/llm_gate.py | 26 ++++++++++++++++-- python/sandbox/neurospace/test_guide.metta | 31 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 python/sandbox/neurospace/test_guide.metta diff --git a/python/sandbox/neurospace/llm_gate.py b/python/sandbox/neurospace/llm_gate.py index 63be0c2b9..6377f1dd5 100644 --- a/python/sandbox/neurospace/llm_gate.py +++ b/python/sandbox/neurospace/llm_gate.py @@ -10,6 +10,23 @@ def to_nested_expr(xs): return E(*list(map(to_nested_expr, xs))) return ValueAtom(xs) +def atom2msg(atom): + if isinstance(atom, ExpressionAtom): + # Avoid () in Expression representation + txt = "" + for ch in atom.get_children(): + txt += atom2msg(ch) + " " + return txt[:-1] + "\n" + if isinstance(atom, GroundedAtom): + if isinstance(atom.get_grounded_type(), ExpressionAtom): + return repr(atom) + if isinstance(atom.get_object(), ValueObject): + # Parse String separately to avoid "" in its repr + v = atom.get_object().value + if isinstance(v, str): + return v.replace("\\n", "\n") + return repr(atom) + def get_message_list(msg_atoms): ''' Convert atoms to ChatGPT messages and flatten a possibly nested message list @@ -23,7 +40,7 @@ def get_message_list(msg_atoms): if ch[0].get_name() == 'Messages': messages += get_message_list(ch[1:]) else: - messages += [{"role": ch[0].get_name(), "content": repr(ch[1])}] + messages += [{"role": ch[0].get_name(), "content": atom2msg(ch[1])}] else: raise TypeError("Messages should be tagged by the role") return messages @@ -93,7 +110,12 @@ def llm(metta: MeTTa, *args): @register_atoms(pass_metta=True) def llmgate_atoms(metta): llmAtom = OperationAtom('llm', lambda *args: llm(metta, *args), unwrap=False) + # Just a helper function if one needs to print from a metta-script + # the message converted from expression to text + msgAtom = OperationAtom('atom2msg', + lambda atom: [ValueAtom(atom2msg(atom))], unwrap=False) return { - r"llm": llmAtom + r"llm": llmAtom, + r"atom2msg": msgAtom } diff --git a/python/sandbox/neurospace/test_guide.metta b/python/sandbox/neurospace/test_guide.metta new file mode 100644 index 000000000..d331744b4 --- /dev/null +++ b/python/sandbox/neurospace/test_guide.metta @@ -0,0 +1,31 @@ +!(extend-py! llm_gate) + +! (bind! &messages (new-space)) + +; All these functions might not be needed if the prompt program is in a separate script +(: system! (-> Atom Atom)) +(: user! (-> Atom Atom)) +(: assistant! (-> Atom Atom)) +(= (system! $msg) (add-atom &messages (system $msg))) +(= (user! $msg) (add-atom &messages (user $msg))) +(= (assistant! $msg) (add-atom &messages (assistant $msg))) + +(= (proverb) "Where there is no guidance, a people falls,\\nbut in an abundance of counselors there is safety.") +(= (book) "Proverbs") +(= (chapter) 11) +(= (verse) 14) + +; Somewhat inconvenient way via states to make in-text replacements with LLM calls +! (let $rewrite (new-state "[continue here]") + (add-atom &self (= (rewrite) $rewrite))) + +! (user! ("Tweak this proverb to apply to model instructions instead.\\n" + (proverb) + "\\n-" (book) (chapter) ":" (verse) + "\\nUPDATED\\n" + "Where there is no guidance" (get-state (rewrite)))) + +! (nop (change-state! (rewrite) (llm (Messages (get-atoms &messages))))) + +! (atom2msg (get-atoms &messages)) + From dc48dc646854eb776c1e65dcb7ee00789dc92123 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Tue, 31 Oct 2023 18:34:06 +0300 Subject: [PATCH 09/33] Fix for non-determenistic messages --- python/sandbox/neurospace/test_guide.metta | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python/sandbox/neurospace/test_guide.metta b/python/sandbox/neurospace/test_guide.metta index d331744b4..990199b37 100644 --- a/python/sandbox/neurospace/test_guide.metta +++ b/python/sandbox/neurospace/test_guide.metta @@ -2,6 +2,10 @@ ! (bind! &messages (new-space)) +(= (messages) + (let $messages (collapse (get-atoms &messages)) + (cons-atom Messages $messages))) + ; All these functions might not be needed if the prompt program is in a separate script (: system! (-> Atom Atom)) (: user! (-> Atom Atom)) @@ -25,7 +29,7 @@ "\\nUPDATED\\n" "Where there is no guidance" (get-state (rewrite)))) -! (nop (change-state! (rewrite) (llm (Messages (get-atoms &messages))))) +! (nop (change-state! (rewrite) (llm (messages)))) ! (atom2msg (get-atoms &messages)) From 168231b61bdec8f1c80f368eceb86bd6669ea794 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 1 Nov 2023 09:56:45 +0900 Subject: [PATCH 10/33] Adding usage error calling `EnvBuilder::set_no_config_dir` after `EnvBuilder::create_config_dir`, so errors are symmetrical --- lib/src/metta/runner/environment.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/metta/runner/environment.rs b/lib/src/metta/runner/environment.rs index 948bf4ccf..ce0e342b8 100644 --- a/lib/src/metta/runner/environment.rs +++ b/lib/src/metta/runner/environment.rs @@ -140,6 +140,9 @@ impl EnvBuilder { /// Configures the Environment not to load nor create any config files pub fn set_no_config_dir(mut self) -> Self { self.no_cfg_dir = true; + if self.create_cfg_dir { + panic!("Fatal Error: set_no_config_dir is incompatible with create_config_dir"); + } if self.env.config_dir.is_some() { panic!("Fatal Error: set_config_dir is incompatible with set_no_config_dir"); } From 036d45442e7fa3278a82552b8b0058602c039314 Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 1 Nov 2023 11:29:01 +0900 Subject: [PATCH 11/33] Moving `==` operator from arithmetics submodule into stdlib, and expanding it to compare other atom types --- lib/src/metta/runner/arithmetics.rs | 14 ++++++++++++-- lib/src/metta/runner/stdlib.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/src/metta/runner/arithmetics.rs b/lib/src/metta/runner/arithmetics.rs index 54ddb7ad3..d9bc24298 100644 --- a/lib/src/metta/runner/arithmetics.rs +++ b/lib/src/metta/runner/arithmetics.rs @@ -7,12 +7,23 @@ use std::fmt::Display; pub const ATOM_TYPE_NUMBER : Atom = sym!("Number"); pub const ATOM_TYPE_BOOL : Atom = sym!("Bool"); -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, Debug)] pub enum Number { Integer(i64), Float(f64), } +impl PartialEq for Number { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Number::Integer(a), Number::Integer(b)) => a == b, + (Number::Integer(a), Number::Float(b)) => (*a as f64) == *b, + (Number::Float(a), Number::Integer(b)) => *a == (*b as f64), + (Number::Float(a), Number::Float(b)) => a == b, + } + } +} + trait IntoNumber { fn into_num(self) -> Number; } @@ -147,7 +158,6 @@ def_binary_number_op!(LessOp, <, ATOM_TYPE_BOOL, Bool); def_binary_number_op!(GreaterOp, >, ATOM_TYPE_BOOL, Bool); def_binary_number_op!(LessEqOp, <=, ATOM_TYPE_BOOL, Bool); def_binary_number_op!(GreaterEqOp, >=, ATOM_TYPE_BOOL, Bool); -def_binary_number_op!(EqualOp, ==, ATOM_TYPE_BOOL, Bool); #[cfg(test)] mod tests { diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 699b8ea65..dfb64e0df 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1083,6 +1083,32 @@ impl Grounded for ChangeStateOp { } } +#[derive(Clone, PartialEq, Debug)] +pub struct EqualOp {} + +impl Display for EqualOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "==") + } +} + +impl Grounded for EqualOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM, ATOM_TYPE_BOOL]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from(concat!(stringify!($op), " expects two arguments")); + let a = args.get(0).ok_or_else(arg_error)?; + let b = args.get(1).ok_or_else(arg_error)?; + + Ok(vec![Atom::gnd(Bool(a == b))]) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} fn regex(regex: &str) -> Regex { Regex::new(regex).unwrap() From 8d15e253e3d0fbb12935a5f82f271a187f7afa2f Mon Sep 17 00:00:00 2001 From: Luke Peterson Date: Wed, 1 Nov 2023 12:02:47 +0900 Subject: [PATCH 12/33] Using initialized runner to parse test spaces and atoms inside types.rs tests, and reverting the common test conveniences to use an empty Tokenizer. --- lib/src/common/test_utils.rs | 10 +++------- lib/src/metta/interpreter2.rs | 3 +-- lib/src/metta/types.rs | 20 +++++++++++++++++++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/src/common/test_utils.rs b/lib/src/common/test_utils.rs index ded97cb68..e33dba350 100644 --- a/lib/src/common/test_utils.rs +++ b/lib/src/common/test_utils.rs @@ -1,23 +1,19 @@ use crate::*; -use crate::metta::runner::*; -use crate::metta::runner::EnvBuilder; -use crate::metta::text::SExprParser; +use crate::metta::text::{Tokenizer, SExprParser}; use crate::space::grounding::GroundingSpace; pub(crate) fn metta_space(text: &str) -> GroundingSpace { - let metta = Metta::new(Some(EnvBuilder::test_env())); let mut space = GroundingSpace::new(); let mut parser = SExprParser::new(text); - while let Some(atom) = parser.parse(&metta.tokenizer().borrow()).unwrap() { + while let Some(atom) = parser.parse(&Tokenizer::new()).unwrap() { space.add(atom); } space } pub(crate) fn metta_atom(atom_str: &str) -> Atom { - let metta = Metta::new(Some(EnvBuilder::test_env())); let mut parser = SExprParser::new(atom_str); - let atom = parser.parse(&metta.tokenizer().borrow()).unwrap().expect("Single atom is expected"); + let atom = parser.parse(&Tokenizer::new()).unwrap().expect("Single atom is expected"); atom } diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index faa9f93e5..39f969d33 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -575,10 +575,9 @@ mod tests { fn interpret_atom_evaluate_pure_expression_variable_name_conflict() { let space = space("(= (foo ($W)) True)"); let result = interpret_atom(&space, atom("(eval (foo $W))", bind!{})); - assert_eq!(result[0].0, metta_atom("True")); + assert_eq!(result[0].0, sym!("True")); } - #[test] fn interpret_atom_evaluate_grounded_expression() { let result = interpret_atom(&space(""), InterpretedAtom(expr!("eval" ({MulXUndefinedType(7)} {6})), bind!{})); diff --git a/lib/src/metta/types.rs b/lib/src/metta/types.rs index 4c32752df..447706e32 100644 --- a/lib/src/metta/types.rs +++ b/lib/src/metta/types.rs @@ -472,7 +472,25 @@ pub fn validate_atom(space: &dyn Space, atom: &Atom) -> bool { mod tests { use super::*; use crate::atom::matcher::atoms_are_equivalent; - use crate::common::test_utils::{metta_atom as atom, metta_space}; + use crate::metta::runner::*; + use crate::metta::text::SExprParser; + + fn metta_space(text: &str) -> GroundingSpace { + let metta = Metta::new(Some(EnvBuilder::test_env())); + let mut space = GroundingSpace::new(); + let mut parser = SExprParser::new(text); + while let Some(atom) = parser.parse(&*metta.tokenizer().borrow()).unwrap() { + space.add(atom); + } + space + } + + fn atom(atom_str: &str) -> Atom { + let metta = Metta::new(Some(EnvBuilder::test_env())); + let mut parser = SExprParser::new(atom_str); + let atom = parser.parse(&*metta.tokenizer().borrow()).unwrap().expect("Single atom is expected"); + atom + } fn grammar_space() -> GroundingSpace { let mut space = GroundingSpace::new(); From ca94de65bf016cc994b7547d5552dc0d8a059ee5 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Thu, 2 Nov 2023 17:58:52 +0300 Subject: [PATCH 13/33] fix let return type --- lib/src/metta/runner/stdlib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index dfb64e0df..652ab02be 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -869,7 +869,7 @@ use std::collections::HashSet; impl Grounded for LetOp { fn type_(&self) -> Atom { // TODO: Undefined for the argument is necessary to make argument reductable. - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_UNDEFINED, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_UNDEFINED, ATOM_TYPE_ATOM, ATOM_TYPE_UNDEFINED]) } fn execute(&self, args: &[Atom]) -> Result, ExecError> { @@ -930,7 +930,7 @@ impl Display for LetVarOp { impl Grounded for LetVarOp { fn type_(&self) -> Atom { // The first argument is an Atom, because it has to be evaluated iteratively - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_ATOM, ATOM_TYPE_ATOM, ATOM_TYPE_UNDEFINED]) } fn execute(&self, args: &[Atom]) -> Result, ExecError> { From f600637921626235825b1e3e1729c0fae9993d1c Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 2 Nov 2023 22:59:51 +0300 Subject: [PATCH 14/33] Implement boolean and, or, and not operations in Rust stdlib --- lib/src/metta/runner/arithmetics.rs | 138 +++++++++++++++++++++++----- lib/src/metta/runner/stdlib.rs | 11 +++ 2 files changed, 124 insertions(+), 25 deletions(-) diff --git a/lib/src/metta/runner/arithmetics.rs b/lib/src/metta/runner/arithmetics.rs index d9bc24298..c46eaf01e 100644 --- a/lib/src/metta/runner/arithmetics.rs +++ b/lib/src/metta/runner/arithmetics.rs @@ -133,10 +133,10 @@ macro_rules! def_binary_number_op { let b = args.get(1).ok_or_else(arg_error)?.as_gnd::().ok_or_else(arg_error)?; let res = match (a, b) { - (Number::Integer(a), Number::Integer(b)) => $cast(a $op b), - (Number::Integer(a), Number::Float(b)) => $cast((*a as f64) $op *b), - (Number::Float(a), Number::Integer(b)) => $cast(*a $op (*b as f64)), - (Number::Float(a), Number::Float(b)) => $cast(a $op b), + (&Number::Integer(a), &Number::Integer(b)) => $cast(a $op b), + (&Number::Integer(a), &Number::Float(b)) => $cast((a as f64) $op b), + (&Number::Float(a), &Number::Integer(b)) => $cast(a $op (b as f64)), + (&Number::Float(a), &Number::Float(b)) => $cast(a $op b), }; Ok(vec![Atom::gnd(res)]) @@ -159,6 +159,66 @@ def_binary_number_op!(GreaterOp, >, ATOM_TYPE_BOOL, Bool); def_binary_number_op!(LessEqOp, <=, ATOM_TYPE_BOOL, Bool); def_binary_number_op!(GreaterEqOp, >=, ATOM_TYPE_BOOL, Bool); +macro_rules! def_binary_bool_op { + ($name:ident, $disp:ident, $op:tt) => { + #[derive(Clone, PartialEq, Debug)] + pub struct $name{} + + impl Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, stringify!($disp)) + } + } + + impl Grounded for $name { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_BOOL, ATOM_TYPE_BOOL, ATOM_TYPE_BOOL]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from(concat!(stringify!($disp), " expects two boolean arguments")); + let &Bool(a) = args.get(0).ok_or_else(arg_error)?.as_gnd::().ok_or_else(arg_error)?; + let &Bool(b) = args.get(1).ok_or_else(arg_error)?.as_gnd::().ok_or_else(arg_error)?; + + Ok(vec![Atom::gnd(Bool(a $op b))]) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } + } + } +} + +def_binary_bool_op!(AndOp, and, &&); +def_binary_bool_op!(OrOp, or, ||); + +#[derive(Clone, PartialEq, Debug)] +pub struct NotOp{} + +impl Display for NotOp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "not") + } +} + +impl Grounded for NotOp { + fn type_(&self) -> Atom { + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_BOOL, ATOM_TYPE_BOOL]) + } + + fn execute(&self, args: &[Atom]) -> Result, ExecError> { + let arg_error = || ExecError::from("not expects one boolean arguments"); + let &Bool(a) = args.get(0).ok_or_else(arg_error)?.as_gnd::().ok_or_else(arg_error)?; + + Ok(vec![Atom::gnd(Bool(!a))]) + } + + fn match_(&self, other: &Atom) -> MatchResultIter { + match_by_equality(self, other) + } +} + #[cfg(test)] mod tests { use super::*; @@ -181,49 +241,77 @@ mod tests { assert_eq!(format!("{}", Bool(false)), "False"); } - macro_rules! assert_number_binary_op { + macro_rules! assert_binary_op { ($name:ident, $a: expr, $b: expr, $r: expr) => { assert_eq!($name{}.execute(&mut vec![Atom::gnd($a), Atom::gnd($b)]), Ok(vec![Atom::gnd($r)])); } } + macro_rules! assert_unary_op { + ($name:ident, $a: expr, $r: expr) => { + assert_eq!($name{}.execute(&mut vec![Atom::gnd($a)]), Ok(vec![Atom::gnd($r)])); + } + } + + #[test] + fn and() { + assert_binary_op!(AndOp, Bool(true), Bool(true), Bool(true)); + assert_binary_op!(AndOp, Bool(true), Bool(false), Bool(false)); + assert_binary_op!(AndOp, Bool(false), Bool(true), Bool(false)); + assert_binary_op!(AndOp, Bool(false), Bool(false), Bool(false)); + } + + #[test] + fn or() { + assert_binary_op!(OrOp, Bool(true), Bool(true), Bool(true)); + assert_binary_op!(OrOp, Bool(true), Bool(false), Bool(true)); + assert_binary_op!(OrOp, Bool(false), Bool(true), Bool(true)); + assert_binary_op!(OrOp, Bool(false), Bool(false), Bool(false)); + } + + #[test] + fn not() { + assert_unary_op!(NotOp, Bool(true), Bool(false)); + assert_unary_op!(NotOp, Bool(false), Bool(true)); + } + #[test] fn sum_op() { - assert_number_binary_op!(SumOp, Number::Integer(40), Number::Integer(2), Number::Integer(42)); - assert_number_binary_op!(SumOp, Number::Integer(40), Number::Float(2.42), Number::Float(42.42)); - assert_number_binary_op!(SumOp, Number::Float(40.42), Number::Integer(2), Number::Float(42.42)); - assert_number_binary_op!(SumOp, Number::Float(40.40), Number::Float(2.02), Number::Float(42.42)); + assert_binary_op!(SumOp, Number::Integer(40), Number::Integer(2), Number::Integer(42)); + assert_binary_op!(SumOp, Number::Integer(40), Number::Float(2.42), Number::Float(42.42)); + assert_binary_op!(SumOp, Number::Float(40.42), Number::Integer(2), Number::Float(42.42)); + assert_binary_op!(SumOp, Number::Float(40.40), Number::Float(2.02), Number::Float(42.42)); } #[test] fn sub_op() { - assert_number_binary_op!(SubOp, Number::Integer(44), Number::Integer(2), Number::Integer(42)); - assert_number_binary_op!(SubOp, Number::Integer(44), Number::Float(2.42), Number::Float(41.58)); - assert_number_binary_op!(SubOp, Number::Float(44.42), Number::Integer(2), Number::Float(42.42)); - assert_number_binary_op!(SubOp, Number::Float(44.5), Number::Float(2.5), Number::Float(42.0)); + assert_binary_op!(SubOp, Number::Integer(44), Number::Integer(2), Number::Integer(42)); + assert_binary_op!(SubOp, Number::Integer(44), Number::Float(2.42), Number::Float(41.58)); + assert_binary_op!(SubOp, Number::Float(44.42), Number::Integer(2), Number::Float(42.42)); + assert_binary_op!(SubOp, Number::Float(44.5), Number::Float(2.5), Number::Float(42.0)); } #[test] fn mul_op() { - assert_number_binary_op!(MulOp, Number::Integer(6), Number::Integer(7), Number::Integer(42)); - assert_number_binary_op!(MulOp, Number::Integer(4), Number::Float(10.5), Number::Float(42.0)); - assert_number_binary_op!(MulOp, Number::Float(10.5), Number::Integer(4), Number::Float(42.0)); - assert_number_binary_op!(MulOp, Number::Float(2.5), Number::Float(16.8), Number::Float(42.0)); + assert_binary_op!(MulOp, Number::Integer(6), Number::Integer(7), Number::Integer(42)); + assert_binary_op!(MulOp, Number::Integer(4), Number::Float(10.5), Number::Float(42.0)); + assert_binary_op!(MulOp, Number::Float(10.5), Number::Integer(4), Number::Float(42.0)); + assert_binary_op!(MulOp, Number::Float(2.5), Number::Float(16.8), Number::Float(42.0)); } #[test] fn div_op() { - assert_number_binary_op!(DivOp, Number::Integer(84), Number::Integer(2), Number::Integer(42)); - assert_number_binary_op!(DivOp, Number::Integer(441), Number::Float(10.5), Number::Float(42.0)); - assert_number_binary_op!(DivOp, Number::Float(84.0), Number::Integer(2), Number::Float(42.0)); - assert_number_binary_op!(DivOp, Number::Float(430.5), Number::Float(10.25), Number::Float(42.0)); + assert_binary_op!(DivOp, Number::Integer(84), Number::Integer(2), Number::Integer(42)); + assert_binary_op!(DivOp, Number::Integer(441), Number::Float(10.5), Number::Float(42.0)); + assert_binary_op!(DivOp, Number::Float(84.0), Number::Integer(2), Number::Float(42.0)); + assert_binary_op!(DivOp, Number::Float(430.5), Number::Float(10.25), Number::Float(42.0)); } #[test] fn mod_op() { - assert_number_binary_op!(ModOp, Number::Integer(85), Number::Integer(43), Number::Integer(42)); - assert_number_binary_op!(ModOp, Number::Integer(85), Number::Float(43.5), Number::Float(41.5)); - assert_number_binary_op!(ModOp, Number::Float(85.5), Number::Integer(43), Number::Float(42.5)); - assert_number_binary_op!(ModOp, Number::Float(85.5), Number::Float(43.5), Number::Float(42.0)); + assert_binary_op!(ModOp, Number::Integer(85), Number::Integer(43), Number::Integer(42)); + assert_binary_op!(ModOp, Number::Integer(85), Number::Float(43.5), Number::Float(41.5)); + assert_binary_op!(ModOp, Number::Float(85.5), Number::Integer(43), Number::Float(42.5)); + assert_binary_op!(ModOp, Number::Float(85.5), Number::Float(43.5), Number::Float(42.0)); } } diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 652ab02be..597c0ced0 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1218,6 +1218,12 @@ pub fn register_rust_tokens(metta: &Metta) { tref.register_token(regex(r">="), move |_| { ge_op.clone() }); let eq_op = Atom::gnd(EqualOp{}); tref.register_token(regex(r"=="), move |_| { eq_op.clone() }); + let and_op = Atom::gnd(AndOp{}); + tref.register_token(regex(r"and"), move |_| { and_op.clone() }); + let or_op = Atom::gnd(OrOp{}); + tref.register_token(regex(r"or"), move |_| { or_op.clone() }); + let not_op = Atom::gnd(NotOp{}); + tref.register_token(regex(r"not"), move |_| { not_op.clone() }); metta.tokenizer().borrow_mut().move_front(&mut rust_tokens); } @@ -1626,4 +1632,9 @@ mod tests { fn test_stdlib_uses_rust_grounded_tokens() { assert_eq!(run_program("!(if True ok nok)"), Ok(vec![vec![Atom::sym("ok")]])); } + + #[test] + fn test_let_op_inside_other_operation() { + assert_eq!(run_program("!(and True (let $x False $x))"), Ok(vec![vec![expr!({Bool(false)})]])); + } } From 859c06e439d0ba34b54f0fe7fd8489dbb51b0924 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Mon, 6 Nov 2023 13:54:12 +0300 Subject: [PATCH 15/33] support for spaces in llm-gate --- python/sandbox/neurospace/llm_gate.py | 140 +++++++++++------- .../sandbox/neurospace/test_guide_func.metta | 20 +++ .../neurospace/test_guide_prompt.metta | 5 + 3 files changed, 115 insertions(+), 50 deletions(-) create mode 100644 python/sandbox/neurospace/test_guide_func.metta create mode 100644 python/sandbox/neurospace/test_guide_prompt.metta diff --git a/python/sandbox/neurospace/llm_gate.py b/python/sandbox/neurospace/llm_gate.py index 6377f1dd5..4dce509cc 100644 --- a/python/sandbox/neurospace/llm_gate.py +++ b/python/sandbox/neurospace/llm_gate.py @@ -27,60 +27,100 @@ def atom2msg(atom): return v.replace("\\n", "\n") return repr(atom) -def get_message_list(msg_atoms): - ''' - Convert atoms to ChatGPT messages and flatten a possibly nested message list - ''' - messages = [] - for msg in msg_atoms: - if isinstance(msg, ExpressionAtom): - ch = msg.get_children() - if len(ch) == 0: - continue - if ch[0].get_name() == 'Messages': - messages += get_message_list(ch[1:]) - else: - messages += [{"role": ch[0].get_name(), "content": atom2msg(ch[1])}] - else: - raise TypeError("Messages should be tagged by the role") - return messages - -def llm(metta: MeTTa, *args): +def get_llm_args(metta: MeTTa, prompt_space: SpaceRef, *args): messages = [] functions = [] - msgs = None + msg_atoms = [] + def __msg_update(m, f, a): + nonlocal messages, functions, msg_atoms + messages += m + functions += f + msg_atoms += [a] for arg in args: - if isinstance(arg, ExpressionAtom): + if isinstance(arg, GroundedAtom) and \ + isinstance(arg.get_object(), SpaceRef): + # FIXME? This will overwrites the current prompt_space if it is set. + # It is convenient to have it here to successfully execute + # (llm &prompt (Functions fn)), when fn is defined in &prompt. + # But (function fn) can also be put in &prompt directly. + # Depending on what is more convenient, this overriding can be changed. + prompt_space = arg.get_object() + __msg_update(*get_llm_args(metta, prompt_space, *prompt_space.get_atoms())) + elif isinstance(arg, ExpressionAtom): ch = arg.get_children() - if len(ch) > 1 and ch[0].get_name() == 'Messages': - msgs = arg - messages += get_message_list(ch[1:]) - if len(ch) > 1 and ch[0].get_name() == 'Functions': - for fn in ch[1:]: - doc = metta.run(f"! (doc {fn})") - if len(doc) == 0: - # TODO: error / warning - continue - # TODO: format is not checked - doc = doc[0][0].get_children() - properties = {} - for par in doc[2].get_children()[1:]: - p = par.get_children() - properties.update({ - p[0].get_name(): { - "type": "string", - "description": p[1].get_object().value, - "enum": list(map(lambda x: x.get_object().value, p[2].get_children())) + if len(ch) > 1: + name = ch[0].get_name() + if name == 'Messages': + __msg_update(*get_llm_args(metta, prompt_space, *ch[1:])) + elif name in ['system', 'user', 'assistant']: + # We have to interpret the message in the main space context, + # if the prompt template is in a separate file and contains + # some external symbols like (user-query) + msg = interpret(metta.space(), ch[1])[0] + messages += [{'role': name, 'content': atom2msg(msg)}] + msg_atoms += [arg] + elif name in ['Functions', 'function']: + for fn in ch[1:]: + doc = None + if prompt_space is not None: + # TODO: Querying for a function description in prompt_space works well, + # but it is useless, because this function cannot be called + # from the main script, so the functional call is not reduced. + # Fixing this requires in general better library management in MeTTa, + # although it can be managed here by interpreting the functional call expression. + # Another approach would be to have load-template, which will import all functions to &self + # (or just to declare function in separate files and load to self, since we may want them + # to be reusable between templates) + r = prompt_space.query(E(S('='), E(S('doc'), fn), V('r'))) + if not r.is_empty(): + doc = r[0]['r'] + if doc is None: + # We use `match` here instead of direct `doc` evaluation + # to evoid non-reduced `doc` + doc = metta.run(f"! (match &self (= (doc {fn}) $r) $r)") + if len(doc) == 0 or len(doc[0]) == 0: + raise RuntimeError(f"No {fn} function description") + doc = doc[0][0] + # TODO: format is not checked + doc = doc.get_children() + properties = {} + for par in doc[2].get_children()[1:]: + p = par.get_children() + properties.update({ + p[0].get_name(): { + "type": "string", + "description": p[1].get_object().value, + "enum": list(map(lambda x: x.get_object().value, p[2].get_children())) + } + }) + functions += [{ + "name": fn.get_name(), + "description": doc[1].get_children()[1].get_object().value, + "parameters": { + "type": "object", + "properties": properties } - }) - functions += [{ - "name": fn.get_name(), - "description": doc[1].get_children()[1].get_object().value, - "parameters": { - "type": "object", - "properties": properties - } - }] + }] + elif name == '=': + # We ignore equalities here: if a space is used to store messages, + # it can contain equalities as well (another approach would be to + # ignore everythins except valid roles) + continue + else: + raise RuntimeError("Unrecognized argument: " + repr(arg)) + else: + # Ignore an empty expression () for convenience, but we need + # to put it back into msg_atoms to keep the structure + msg_atoms += [arg] + else: + raise RuntimeError("Unrecognized argument: " + repr(arg)) + # Do not wrap a single message into Message (necessary to avoid double + # wrapping of single Message argument) + return messages, functions, \ + msg_atoms[0] if len(msg_atoms) == 1 else E(S('Messages'), *msg_atoms) + +def llm(metta: MeTTa, *args): + messages, functions, msgs_atom = get_llm_args(metta, None, *args) #print(messages) #return [] if functions==[]: @@ -104,7 +144,7 @@ def llm(metta: MeTTa, *args): fs = S(response_message["function_call"]["name"]) args = response_message["function_call"]["arguments"] args = json.loads(args) - return [E(fs, to_nested_expr(list(args.values())), msgs)] + return [E(fs, to_nested_expr(list(args.values())), msgs_atom)] return [ValueAtom(response_message['content'])] @register_atoms(pass_metta=True) diff --git a/python/sandbox/neurospace/test_guide_func.metta b/python/sandbox/neurospace/test_guide_func.metta new file mode 100644 index 000000000..224d28861 --- /dev/null +++ b/python/sandbox/neurospace/test_guide_func.metta @@ -0,0 +1,20 @@ +!(extend-py! llm_gate) + +!(import! &msgs test_guide_prompt.metta) + +; Function for calls still cannot be put into the prompt space, because +; they will not be evaluated, when the functional call is evoked here +(= (doc calc_math) + (Doc + (description "You should call this function with a mathematical expression in Scheme") + (parameters + (expression "Mathematical expression in Scheme" ()) + )) +) +; This is another limitation: LLM output should somehow be transormed to MeTTa expressions +(= (calc_math $expr $msgs) + ($expr is not evaluated, because it is a string atm)) + +(= (user-query) "What is the result of 111102 + 18333?") + +! (llm &msgs) diff --git a/python/sandbox/neurospace/test_guide_prompt.metta b/python/sandbox/neurospace/test_guide_prompt.metta new file mode 100644 index 000000000..c30944571 --- /dev/null +++ b/python/sandbox/neurospace/test_guide_prompt.metta @@ -0,0 +1,5 @@ +(system "Answer the user question. Try to reason carefully.") + +(user (user-query)) + +(function calc_math) From ef91eda87aedcea917ade8eca514a191d778a398 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Mon, 6 Nov 2023 15:59:35 +0300 Subject: [PATCH 16/33] simple but useful postprocessing added --- python/sandbox/neurospace/llm_gate.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python/sandbox/neurospace/llm_gate.py b/python/sandbox/neurospace/llm_gate.py index 4dce509cc..23044d968 100644 --- a/python/sandbox/neurospace/llm_gate.py +++ b/python/sandbox/neurospace/llm_gate.py @@ -159,3 +159,13 @@ def llmgate_atoms(metta): r"atom2msg": msgAtom } + +def str_find_all(str, values): + return list(filter(lambda v: v in str, values)) + +@register_atoms +def postproc_atoms(): + strfindAtom = OperationAtom('str-find-all', str_find_all) + return { + r"str-find-all": strfindAtom, + } From c2b523ce1f1fa80acae7a1a32f5f3dd202aa6eb4 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Tue, 7 Nov 2023 11:27:40 +0300 Subject: [PATCH 17/33] add <= and >= to Python stdlib --- python/hyperon/stdlib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/hyperon/stdlib.py b/python/hyperon/stdlib.py index a841e92c5..1069ab68a 100644 --- a/python/hyperon/stdlib.py +++ b/python/hyperon/stdlib.py @@ -41,6 +41,8 @@ def bool_ops(): ['$t', '$t', 'Bool'], unwrap=False) greaterAtom = OperationAtom('>', lambda a, b: a > b, ['Number', 'Number', 'Bool']) lessAtom = OperationAtom('<', lambda a, b: a < b, ['Number', 'Number', 'Bool']) + greaterEqAtom = OperationAtom('>=', lambda a, b: a >= b, ['Number', 'Number', 'Bool']) + lessEqAtom = OperationAtom('<=', lambda a, b: a <= b, ['Number', 'Number', 'Bool']) orAtom = OperationAtom('or', lambda a, b: a or b, ['Bool', 'Bool', 'Bool']) andAtom = OperationAtom('and', lambda a, b: a and b, ['Bool', 'Bool', 'Bool']) notAtom = OperationAtom('not', lambda a: not a, ['Bool', 'Bool']) @@ -48,6 +50,8 @@ def bool_ops(): r"==": equalAtom, r"<": lessAtom, r">": greaterAtom, + r"<=": lessEqAtom, + r">=": greaterEqAtom, r"or": orAtom, r"and": andAtom, r"not": notAtom From d3d9d10f629829428caab8cb93b14b4b1f78c61c Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Tue, 7 Nov 2023 17:39:44 +0300 Subject: [PATCH 18/33] fix return type of car/cdr-atom --- lib/src/metta/runner/stdlib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index 597c0ced0..f0be2b468 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -325,7 +325,7 @@ impl Display for CarAtomOp { impl Grounded for CarAtomOp { fn type_(&self) -> Atom { - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_ATOM]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_UNDEFINED]) } fn execute(&self, args: &[Atom]) -> Result, ExecError> { @@ -352,7 +352,7 @@ impl Display for CdrAtomOp { impl Grounded for CdrAtomOp { fn type_(&self) -> Atom { - Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_ATOM]) + Atom::expr([ARROW_SYMBOL, ATOM_TYPE_EXPRESSION, ATOM_TYPE_UNDEFINED]) } fn execute(&self, args: &[Atom]) -> Result, ExecError> { From c837f30eaaad5cd4b0d3b4f4339b164889522e45 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 8 Nov 2023 18:51:49 +0300 Subject: [PATCH 19/33] Stop caching result of the grounded operations --- lib/src/metta/interpreter.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/src/metta/interpreter.rs b/lib/src/metta/interpreter.rs index a77fa2128..6cac52ade 100644 --- a/lib/src/metta/interpreter.rs +++ b/lib/src/metta/interpreter.rs @@ -317,13 +317,14 @@ fn is_variable_op(expr: &ExpressionAtom) -> bool { } } -fn has_grounded_sub_expr(expr: &Atom) -> bool { - return SubexprStream::from_expr(expr.clone(), TOP_DOWN_DEPTH_WALK) - .any(|sub| if let Atom::Expression(sub) = sub { - is_grounded_op(&sub) - } else { - panic!("Expression is expected"); - }); +fn has_grounded_sub_expr(expr: &ExpressionAtom) -> bool { + return is_grounded_op(expr) || + SubexprStream::from_expr(Atom::Expression(expr.clone()), TOP_DOWN_DEPTH_WALK) + .any(|sub| if let Atom::Expression(sub) = sub { + is_grounded_op(&sub) + } else { + panic!("Expression is expected"); + }); } fn interpret_as_type_plan<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<'a, T>, @@ -520,8 +521,8 @@ fn call_op<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<'a, T>, input: In }).collect(); return_cached_result_plan(result) } else { - if let Atom::Expression(_) = input.atom() { - if !has_grounded_sub_expr(input.atom()) { + if let Atom::Expression(expr) = input.atom() { + if !has_grounded_sub_expr(expr) { let key = input.atom().clone(); StepResult::execute(SequencePlan::new( OrPlan::new( From 499d9f2e96746ad92746bd36a848e2d61c12cba4 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 9 Nov 2023 19:33:26 +0300 Subject: [PATCH 20/33] Disable variable_operation feature by default Add comments to the features. --- README.md | 6 ++++++ lib/Cargo.toml | 9 +++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6faeb6f41..d5fb74fb9 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,12 @@ cargo build cargo test ``` +The experimental features can be enabled by editing +[Cargo.toml](./lib/Cargo.toml) file before compilation or by using `--features` +[command line option](https://doc.rust-lang.org/cargo/reference/features.html#command-line-feature-options). +See comments in the `[features]` section of the file for the features +descriptions. + Run examples: ``` cargo run --example sorted_list diff --git a/lib/Cargo.toml b/lib/Cargo.toml index a73510e52..274ac4ebd 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -18,7 +18,8 @@ path = "src/lib.rs" crate-type = ["lib"] [features] -default = ["variable_operation"] -#default = ["variable_operation", "minimal"] -minimal = [] -variable_operation = [] +default = [] +minimal = [] # enables minimal MeTTa interpreter +variable_operation = [] # enables evaluation of the expressions which have + # a variable on the first position, doesn't affect + # minimal MeTTa functionality From e4ede3382f812a3d9f19b950eb2511093cce33b1 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Fri, 10 Nov 2023 10:15:08 +0300 Subject: [PATCH 21/33] Add comment how to enable features into Cargo.toml --- lib/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 274ac4ebd..f9b4e9c5c 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -19,6 +19,8 @@ crate-type = ["lib"] [features] default = [] +# Add one of the features below into default list to enable. +# See https://doc.rust-lang.org/cargo/reference/features.html#the-features-section minimal = [] # enables minimal MeTTa interpreter variable_operation = [] # enables evaluation of the expressions which have # a variable on the first position, doesn't affect From e6a072e897f0641d5da69e3784ccc84da15f65df Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 13 Nov 2023 11:54:52 +0300 Subject: [PATCH 22/33] new das api added to dasgate example --- python/sandbox/das_gate/dasgate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/python/sandbox/das_gate/dasgate.py b/python/sandbox/das_gate/dasgate.py index 091245984..8fe24c7db 100644 --- a/python/sandbox/das_gate/dasgate.py +++ b/python/sandbox/das_gate/dasgate.py @@ -3,6 +3,7 @@ from hyperon_das import DistributedAtomSpace +from hyperon_das.utils import QueryOutputFormat from hyperon_das.pattern_matcher import ( Link, @@ -54,11 +55,11 @@ def _handle2atom(self, h): def query(self, query_atom): query = self._atom2query(query_atom) - answer = PatternMatchingAnswer() + answer = self.das.query(query, {'return_type': QueryOutputFormat.HANDLE}) new_bindings_set = BindingsSet.empty() - if not query.matched(self.das.db, answer): + if answer is None: return new_bindings_set - for a in answer.assignments: + for a in answer['mapping']: bindings = Bindings() for var, val in a.mapping.items(): # remove '$', because it is automatically added From d56714054961e0a65505abb49e38172f20adf7cb Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Mon, 13 Nov 2023 13:02:02 +0300 Subject: [PATCH 23/33] Update dasgate.py `'toplevel_only': True` seems to work and changes the results of the tests in a right way --- python/sandbox/das_gate/dasgate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/sandbox/das_gate/dasgate.py b/python/sandbox/das_gate/dasgate.py index 8fe24c7db..c9de492ad 100644 --- a/python/sandbox/das_gate/dasgate.py +++ b/python/sandbox/das_gate/dasgate.py @@ -55,7 +55,8 @@ def _handle2atom(self, h): def query(self, query_atom): query = self._atom2query(query_atom) - answer = self.das.query(query, {'return_type': QueryOutputFormat.HANDLE}) + answer = self.das.query(query, + {'return_type': QueryOutputFormat.HANDLE, 'toplevel_only': True}) new_bindings_set = BindingsSet.empty() if answer is None: return new_bindings_set From 4ae25c55ebcec564bade0d8f1529dee0ee2e22c4 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Mon, 13 Nov 2023 20:47:35 +0300 Subject: [PATCH 24/33] turn module path to file path in extend-py --- python/hyperon/runner.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py index d5e5d2120..98e58f6f4 100644 --- a/python/hyperon/runner.py +++ b/python/hyperon/runner.py @@ -133,9 +133,10 @@ def load_py_module_from_mod_or_file(self, mod_name): if not isinstance(mod_name, str): mod_name = repr(mod_name) mod = MeTTa.load_py_module(self, mod_name) - if (mod is None): + if mod is None: # If that failed, try and load the module from a file - file_name = mod_name + ".py" + file_name = mod_name if ".py" in mod_name else \ + mod_name.replace('.', '/') + ".py" # Check each search path directory in order, until we find the module we're looking for num_search_paths = hp.metta_search_path_cnt(self.cmetta) From 787c9db4db1ec286c3c6a6ffd55e0310445c466a Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Mon, 13 Nov 2023 20:53:09 +0300 Subject: [PATCH 25/33] os.sep --- python/hyperon/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py index 98e58f6f4..cdb67bfcd 100644 --- a/python/hyperon/runner.py +++ b/python/hyperon/runner.py @@ -136,7 +136,7 @@ def load_py_module_from_mod_or_file(self, mod_name): if mod is None: # If that failed, try and load the module from a file file_name = mod_name if ".py" in mod_name else \ - mod_name.replace('.', '/') + ".py" + mod_name.replace('.', os.sep) + ".py" # Check each search path directory in order, until we find the module we're looking for num_search_paths = hp.metta_search_path_cnt(self.cmetta) From 001fbdb0bd0fb011a77900e0a1fc741b6bb24858 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Mon, 13 Nov 2023 21:24:22 +0300 Subject: [PATCH 26/33] remove brackets --- python/hyperon/runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py index cdb67bfcd..0c01b089c 100644 --- a/python/hyperon/runner.py +++ b/python/hyperon/runner.py @@ -142,15 +142,15 @@ def load_py_module_from_mod_or_file(self, mod_name): num_search_paths = hp.metta_search_path_cnt(self.cmetta) search_path_idx = 0 found_path = None - while (search_path_idx < num_search_paths): + while search_path_idx < num_search_paths: search_path = hp.metta_nth_search_path(self.cmetta, search_path_idx) test_path = os.path.join(search_path, file_name) - if (os.path.exists(test_path)): + if os.path.exists(test_path): found_path = test_path break search_path_idx += 1 - if (found_path is not None): + if found_path is not None: MeTTa.load_py_module_from_path(self, mod_name, found_path) else: raise RuntimeError("Failed to load module " + mod_name + "; could not locate file: " + file_name) From 45e94a2522996fd4d2ef62009a5e3430fc81afb0 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 15 Nov 2023 11:55:01 +0300 Subject: [PATCH 27/33] Remove Bindings::resolve_and_remove method --- c/src/atom.rs | 15 --------------- lib/src/atom/matcher.rs | 19 ------------------- lib/src/metta/interpreter.rs | 5 ++--- lib/src/metta/interpreter2.rs | 7 +++---- lib/src/metta/types.rs | 13 +++++++++++-- python/hyperon/atoms.py | 5 ----- python/hyperonpy.cpp | 5 ----- python/tests/test_bindings.py | 15 +-------------- 8 files changed, 17 insertions(+), 67 deletions(-) diff --git a/c/src/atom.rs b/c/src/atom.rs index f36e73ef2..2a2456c72 100644 --- a/c/src/atom.rs +++ b/c/src/atom.rs @@ -1155,21 +1155,6 @@ pub extern "C" fn bindings_resolve(bindings: *const bindings_t, var_name: *const bindings.resolve(&var).into() } -/// @brief Returns the atom bound to the supplied variable name in the `bindings_t`, and removes it from the `bindings_t` -/// @ingroup matching_group -/// @param[in] bindings A pointer to the `bindings_t` to inspect -/// @param[in] var_name A NULL-terminated C-style string containing the name of the variable -/// @return The `atom_t` representing the atom that corresponds to the specified variable, or a NULL `atom_ref_t` if the variable is not present. -/// @note The caller must take ownership responsibility for the returned `atom_t`, if it is not NULL -/// -#[no_mangle] -pub extern "C" fn bindings_resolve_and_remove(bindings: *mut bindings_t, var_name: *const c_char) -> atom_t { - let bindings = unsafe{ &mut*bindings }.borrow_mut(); - let var = VariableAtom::new(cstr_into_string(var_name)); - - bindings.resolve_and_remove(&var).into() -} - /// @brief Merges two `bindings_t` Bindings frames together into a Bindings Set /// @ingroup matching_group /// @param[in] _self The first `bindings_t` to merge. Ownership of this argument is taken by this function diff --git a/lib/src/atom/matcher.rs b/lib/src/atom/matcher.rs index 5a1fd2561..1eaa2c3b0 100644 --- a/lib/src/atom/matcher.rs +++ b/lib/src/atom/matcher.rs @@ -429,25 +429,6 @@ impl Bindings { var_by_id } - /// Resolve variable, remove it from [Bindings] and return result. - /// - /// # Examples - /// - /// ``` - /// use hyperon::*; - /// - /// let mut bindings = bind!{ x: expr!(y), y: expr!("A" z), z: expr!("B") }; - /// - /// assert_eq!(bindings.resolve_and_remove(&VariableAtom::new("x")), Some(expr!("A" "B"))); - /// assert_eq!(bindings.resolve(&VariableAtom::new("x")), None); - /// assert_eq!(bindings.resolve(&VariableAtom::new("y")), Some(expr!("A" "B"))); - /// ``` - pub fn resolve_and_remove(&mut self, var: &VariableAtom) -> Option { - let result = self.resolve(&var); - self.remove(&var); - result - } - fn remove(&mut self, var: &VariableAtom) -> Option { match self.id_by_var.remove(var) { None => None, diff --git a/lib/src/metta/interpreter.rs b/lib/src/metta/interpreter.rs index 6cac52ade..66f648772 100644 --- a/lib/src/metta/interpreter.rs +++ b/lib/src/metta/interpreter.rs @@ -624,9 +624,8 @@ fn match_op<'a, T: SpaceRef<'a>>(context: InterpreterContextRef<'a, T>, input: I let mut query_bindings = context.space.query(&query); let results: Vec = query_bindings .drain(0..) - .map(|mut query_binding| { - let result = query_binding.resolve_and_remove(&var_x).unwrap(); - let result = apply_bindings_to_atom(&result, &query_binding); + .map(|query_binding| { + let result = apply_bindings_to_atom(&Atom::Variable(var_x.clone()), &query_binding); // TODO: sometimes we apply bindings twice: first time here, // second time when inserting matched argument into nesting // expression. It should be enough doing it only once. diff --git a/lib/src/metta/interpreter2.rs b/lib/src/metta/interpreter2.rs index 39f969d33..3cc863298 100644 --- a/lib/src/metta/interpreter2.rs +++ b/lib/src/metta/interpreter2.rs @@ -363,14 +363,13 @@ fn query<'a, T: SpaceRef<'a>>(space: T, atom: Atom, bindings: Bindings) -> Vec Vec { // TODO: query should check that sub type is a type and not another typed symbol let var_x = VariableAtom::new("X").make_unique(); let mut super_types = space.query(&isa_query(&sub_type, &Atom::Variable(var_x.clone()))); - super_types.drain(0..).map(|mut bindings| { bindings.resolve_and_remove(&var_x).unwrap() }).collect() + let atom_x = Atom::Variable(var_x); + super_types.drain(0..).map(|bindings| { apply_bindings_to_atom(&atom_x, &bindings) }).collect() } fn add_super_types(space: &dyn Space, sub_types: &mut Vec, from: usize) { @@ -107,7 +108,15 @@ pub fn is_func(typ: &Atom) -> bool { fn query_types(space: &dyn Space, atom: &Atom) -> Vec { let var_x = VariableAtom::new("X").make_unique(); let mut types = query_has_type(space, atom, &Atom::Variable(var_x.clone())); - let mut types = types.drain(0..).filter_map(|mut bindings| { bindings.resolve_and_remove(&var_x) }).collect(); + let atom_x = Atom::Variable(var_x); + let mut types = types.drain(0..).filter_map(|bindings| { + let atom = apply_bindings_to_atom(&atom_x, &bindings); + if atom_x == atom { + None + } else { + Some(atom) + } + }).collect(); add_super_types(space, &mut types, 0); types } diff --git a/python/hyperon/atoms.py b/python/hyperon/atoms.py index 0e2beddd7..13f4e1175 100644 --- a/python/hyperon/atoms.py +++ b/python/hyperon/atoms.py @@ -498,11 +498,6 @@ def resolve(self, var_name: str) -> Union[Atom, None]: raw_atom = hp.bindings_resolve(self.cbindings, var_name) return None if raw_atom is None else Atom._from_catom(raw_atom) - def resolve_and_remove(self, var_name: str) -> Union[Atom, None]: - """Finds and removes the atom for a given variable name""" - raw_atom = hp.bindings_resolve_and_remove(self.cbindings, var_name) - return None if raw_atom is None else Atom._from_catom(raw_atom) - def iterator(self): """Returns an iterator over the variable-atom pairs in the bindings""" res = hp.bindings_list(self.cbindings) diff --git a/python/hyperonpy.cpp b/python/hyperonpy.cpp index f23a979c2..c3a3971a7 100644 --- a/python/hyperonpy.cpp +++ b/python/hyperonpy.cpp @@ -601,11 +601,6 @@ PYBIND11_MODULE(hyperonpy, m) { return atom_is_null(&res) ? nonstd::nullopt : nonstd::optional(CAtom(res)); }, "Resolve" ); - m.def("bindings_resolve_and_remove", [](CBindings bindings, char const* varName) -> nonstd::optional { - auto const res = bindings_resolve_and_remove(bindings.ptr(), varName); - return atom_is_null(&res) ? nonstd::nullopt : nonstd::optional(CAtom(res)); - }, "Resolve and remove" ); - m.def("bindings_to_str", [](CBindings bindings) { return func_to_string((write_to_buf_func_t)&bindings_to_str, bindings.ptr()); }, "Convert bindings to human readable string"); diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index 3a3fd65ce..10cb82f32 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -75,19 +75,6 @@ def test_bindings_resolve(self): atom_resolved = self.bindings.resolve("a") self.assertEqual(atom_expected, atom_resolved) - def test_bindings_resolve_and_remove(self): - self.assertIsNone(self.emptyBindings.resolve_and_remove("a")) - self.assertIsNone(self.bindings.resolve_and_remove("XYXY")) - - atom_expected_first = S("b") - atom_expected_second = S("y") - atom_resolved_first = self.bindings.resolve_and_remove("a") - atom_resolved_second = self.bindings.resolve_and_remove("x") - - self.assertTrue(self.bindings.is_empty()) - self.assertEqual(atom_expected_first, atom_resolved_first) - self.assertEqual(atom_expected_second, atom_resolved_second) - def test_bindings_iterator(self): pass # uncomment below as sort in bindings become stable. @@ -120,7 +107,7 @@ def test_bindings_set(self): set.add_var_binding(V("a"), S("A")) self.assertFalse(set.is_single()) self.assertFalse(set.is_empty()) - self.assertEqual(set, no_longer_empty_set); + self.assertEqual(set, no_longer_empty_set); new_bindings = Bindings() new_bindings.add_var_binding(V("a"), S("A")) From 810456c730bb9e0bad18e99ec93287df8a8d853e Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Wed, 15 Nov 2023 12:41:57 +0300 Subject: [PATCH 28/33] Fix unit test to take into account global counter state --- lib/src/atom/matcher.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/atom/matcher.rs b/lib/src/atom/matcher.rs index 1eaa2c3b0..4433057af 100644 --- a/lib/src/atom/matcher.rs +++ b/lib/src/atom/matcher.rs @@ -1250,7 +1250,8 @@ mod test { #[test] fn match_variable_with_unique_itself() { - let x_uniq = Atom::Variable(VariableAtom::new_id("x", 1)); + let last_id = VariableAtom::new("x").make_unique().id; + let x_uniq = Atom::Variable(VariableAtom::new_id("x", last_id + 1)); assert_match( make_variables_unique(expr!(("A" x) ("B" x))), expr!(("A" x) z ), From af9af9fc9213afc4c5efb0ec54d0a3b8bd4a6fcf Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Wed, 15 Nov 2023 18:09:59 +0300 Subject: [PATCH 29/33] relative path imports --- python/hyperon/runner.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py index 0c01b089c..462b6408c 100644 --- a/python/hyperon/runner.py +++ b/python/hyperon/runner.py @@ -116,6 +116,7 @@ def load_py_module(self, name): name = repr(name) try: mod = import_module(name) + print("HERE2 ", name, ":", mod) self.pymods[name] = mod for n in dir(mod): obj = getattr(mod, n) @@ -123,6 +124,7 @@ def load_py_module(self, name): obj(self) return mod except: + print("HERE1111: ", name) return None def load_py_module_from_mod_or_file(self, mod_name): @@ -160,6 +162,7 @@ def load_py_module_from_path(self, mod_name, path): spec = importlib.util.spec_from_file_location(mod_name, path) module = importlib.util.module_from_spec(spec) + mod_name = mod_name.split(os.sep)[-1] sys.modules[mod_name] = module spec.loader.exec_module(module) MeTTa.load_py_module(self, mod_name) From 17e36dba2e8c30b62562c0859ad1a94670e42990 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Wed, 15 Nov 2023 18:13:38 +0300 Subject: [PATCH 30/33] cleanup --- python/hyperon/runner.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/hyperon/runner.py b/python/hyperon/runner.py index 462b6408c..ed1961e04 100644 --- a/python/hyperon/runner.py +++ b/python/hyperon/runner.py @@ -116,7 +116,6 @@ def load_py_module(self, name): name = repr(name) try: mod = import_module(name) - print("HERE2 ", name, ":", mod) self.pymods[name] = mod for n in dir(mod): obj = getattr(mod, n) @@ -124,7 +123,6 @@ def load_py_module(self, name): obj(self) return mod except: - print("HERE1111: ", name) return None def load_py_module_from_mod_or_file(self, mod_name): From d494cf978cf1648fa5a447bf8b25c4f0f0e5ac55 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Wed, 15 Nov 2023 19:11:58 +0300 Subject: [PATCH 31/33] remove llm_gate --- python/sandbox/neurospace/test_assist.metta | 100 ------------------ python/sandbox/neurospace/test_guide.metta | 35 ------ .../sandbox/neurospace/test_guide_func.metta | 20 ---- .../neurospace/test_guide_prompt.metta | 5 - 4 files changed, 160 deletions(-) delete mode 100644 python/sandbox/neurospace/test_assist.metta delete mode 100644 python/sandbox/neurospace/test_guide.metta delete mode 100644 python/sandbox/neurospace/test_guide_func.metta delete mode 100644 python/sandbox/neurospace/test_guide_prompt.metta diff --git a/python/sandbox/neurospace/test_assist.metta b/python/sandbox/neurospace/test_assist.metta deleted file mode 100644 index a0d1cba24..000000000 --- a/python/sandbox/neurospace/test_assist.metta +++ /dev/null @@ -1,100 +0,0 @@ -!(extend-py! llm_gate) - -(= (append-expr $xs $x) - (collapse (superpose ((superpose $xs) $x)))) - -; We could use $_, so this message would be automatically added to any prompt, -; but in this case it would be difficult to avoid its duplication -(= (message init) - (system "You are the SingularityNet AI platform and marketplace assistant")) - -; We need to duplicate this message -(= (message platform-desc) - (message init)) - -(= (message platform-desc) - (system "SingularityNET (SNET) is an open and decentralized network of AI services made accessible through the Blockchain. Developers publish their services to the SingularityNET network, where they can be used by anyone with an internet connection. Developers are able to charge for the use of their services using the native AGIX token. -Services can span the entire gamut of offerings in artificial intelligence and machine learning. Services can provide inference or model training across myriad domains such as image/video, speech, text, time-series, bio-AI, network analysis, etc. The services can be as simple as wrapping a well-known algorithm such as A* path planning, a complete end-to-end solution for an industry problem, or a standalone AI application. Developers can also deploy autonomous AI agents that interoperate with other services on the network. -The SingularityNET platform contains a number of critical components that work together to enable a decentralized network of AI services to flourish. The core components are designed to allow for a functional, scalable, and extensible system. We arrived at the current architecture through a careful process, guided by a few key decisions governing Blockchain interactions, AI service integration, and abstraction and by the goal of building an AI marketplace that is both open and compliant with regulatory and legal requirements. -First, we made the conscious choice to minimize our dependence on our current Blockchain, Ethereum. Both conceptual and practical issues motivated this decision. Conceptually, we desire to be Blockchain-agnostic and, if necessary, will consider building our own consensus algorithm based on reputation. The speed, reliability, and costs of Ethereum Blockchain interactions dictate that any scalable system built on top of it must minimize gas costs and the delays introduced by block-mining time. These decisions are reflected in our use of tools to abstract away all Blockchain interactions (the daemon, CLI, and SDK) and in our use of a multi-party escrow contract and atomic unidirectional channels for payments. -Second, on AI services integration, we wanted to abstract away as much of the network as possible, in order to reduce the learning curve and minimize the overhead associated with providing AI services via the network. This abstraction is achieved with a single flexible tool, the daemon, that will help us provide scalability, robustness, distribution, and management features to the entire community. -Finally, to make our marketplace compliant with regulations without compromising on openness, we implemented it separately from our fully decentralized registry of AI services currently available on the Blockchain.")) - -(= (message service-list) - (system "Select the service, which is most relevant to the user question. The list of services with descriptions - style-transfer : Provide two images and use this service to transfer the artistic-style of one image to the second image provided. - image2text-handwritten : The service receives an image of a English-language handwritten text line and outputs the result of image recognition as a text sequence. - speech-emotions : Submit a WAV file (up to 4 MB and no longer than 90 seconds) with English speech and get an emotion label from the provided WAV file. - super-resolution : The service takes a low-resolution image in binary format, uses it as input for a pre-trained model, and outputs the result as a higher-quality image. - ")) - -(= (function init) - set_subgoal) - -(= (doc set_subgoal) - (Doc - (description "You should always call this function to answer the user") - (parameters - (goal "The goal of the user" ("greeting" "general question about platform" "search for particular service" "service list")) - )) -) - -; We suppose that the second argument has a certain structure built by compose-prompt -; so it can be deconstructed by the corresponding argument pattern -(= (set_subgoal - ("general question about platform") - (Messages $history $prev-state-msg $user-msg)) - (llm (compose-prompt platform-desc $history $user-msg)) - ; (llm (append-expr $msgs (message platform-desc)) -) - -; Extracting components of input messages, ignoring $prev-state-msg and calling -; compose-prompt is a standard pattern to keep the user message at the end -; of the message list inserting new system messages in-between -(= (set_subgoal - ("search for particular service") - (Messages $history $prev-state-msg $user-msg)) - (llm - (compose-prompt service-list $history $user-msg) - (Functions describe_service)) -) - -; In this example, we just append the list of messages with the new system message -; for further elaboration on the service. -(= (describe_service ($args) $msgs) - (llm - (append-expr $msgs (system ("You selected the service " $args " Please describe it."))))) -(= (doc describe_service) - (Doc - (description "You should call this function to describe the service.") - (parameters - (service "The service to be described" - ("style-transfer" "image2text-handwritten" "speech-emotions" "super-resolution")) - )) -) - -(= (compose-prompt $state $history $new-msg) - (let $msgs-state (collapse (message $state)) - (Messages - $history - (cons-atom Messages $msgs-state) - $new-msg))) - -(= (compose-functions $state) - (let $fn-calls (collapse (function $state)) - (cons-atom Functions $fn-calls))) - -; TODO? Assuming that $user-msg is not wrapped into (user ...) is convenient -; for the first call, but not convenient for using `respond` from functional calls -(= (respond $state $history $user-msg) - (llm - (compose-prompt $state $history (user $user-msg)) - (compose-functions $state) - )) - -; !(compose-prompt init () (user "What services do you have on the platform?")) -; !(compose-functions init) -;!(respond init () "What services do you have on the platform?") -;!(respond init () "What can I use for style transfer?") -;!(respond init () "Hello. What can you do?") -!(respond init () "What is the SingularityNET platform?") diff --git a/python/sandbox/neurospace/test_guide.metta b/python/sandbox/neurospace/test_guide.metta deleted file mode 100644 index 990199b37..000000000 --- a/python/sandbox/neurospace/test_guide.metta +++ /dev/null @@ -1,35 +0,0 @@ -!(extend-py! llm_gate) - -! (bind! &messages (new-space)) - -(= (messages) - (let $messages (collapse (get-atoms &messages)) - (cons-atom Messages $messages))) - -; All these functions might not be needed if the prompt program is in a separate script -(: system! (-> Atom Atom)) -(: user! (-> Atom Atom)) -(: assistant! (-> Atom Atom)) -(= (system! $msg) (add-atom &messages (system $msg))) -(= (user! $msg) (add-atom &messages (user $msg))) -(= (assistant! $msg) (add-atom &messages (assistant $msg))) - -(= (proverb) "Where there is no guidance, a people falls,\\nbut in an abundance of counselors there is safety.") -(= (book) "Proverbs") -(= (chapter) 11) -(= (verse) 14) - -; Somewhat inconvenient way via states to make in-text replacements with LLM calls -! (let $rewrite (new-state "[continue here]") - (add-atom &self (= (rewrite) $rewrite))) - -! (user! ("Tweak this proverb to apply to model instructions instead.\\n" - (proverb) - "\\n-" (book) (chapter) ":" (verse) - "\\nUPDATED\\n" - "Where there is no guidance" (get-state (rewrite)))) - -! (nop (change-state! (rewrite) (llm (messages)))) - -! (atom2msg (get-atoms &messages)) - diff --git a/python/sandbox/neurospace/test_guide_func.metta b/python/sandbox/neurospace/test_guide_func.metta deleted file mode 100644 index 224d28861..000000000 --- a/python/sandbox/neurospace/test_guide_func.metta +++ /dev/null @@ -1,20 +0,0 @@ -!(extend-py! llm_gate) - -!(import! &msgs test_guide_prompt.metta) - -; Function for calls still cannot be put into the prompt space, because -; they will not be evaluated, when the functional call is evoked here -(= (doc calc_math) - (Doc - (description "You should call this function with a mathematical expression in Scheme") - (parameters - (expression "Mathematical expression in Scheme" ()) - )) -) -; This is another limitation: LLM output should somehow be transormed to MeTTa expressions -(= (calc_math $expr $msgs) - ($expr is not evaluated, because it is a string atm)) - -(= (user-query) "What is the result of 111102 + 18333?") - -! (llm &msgs) diff --git a/python/sandbox/neurospace/test_guide_prompt.metta b/python/sandbox/neurospace/test_guide_prompt.metta deleted file mode 100644 index c30944571..000000000 --- a/python/sandbox/neurospace/test_guide_prompt.metta +++ /dev/null @@ -1,5 +0,0 @@ -(system "Answer the user question. Try to reason carefully.") - -(user (user-query)) - -(function calc_math) From 77302d420620287acc00eb114e30dd25b5d87987 Mon Sep 17 00:00:00 2001 From: Alexey Potapov Date: Thu, 16 Nov 2023 10:48:33 +0300 Subject: [PATCH 32/33] follow-up rm llm_gate.py --- python/sandbox/neurospace/llm_gate.py | 171 -------------------------- 1 file changed, 171 deletions(-) delete mode 100644 python/sandbox/neurospace/llm_gate.py diff --git a/python/sandbox/neurospace/llm_gate.py b/python/sandbox/neurospace/llm_gate.py deleted file mode 100644 index 23044d968..000000000 --- a/python/sandbox/neurospace/llm_gate.py +++ /dev/null @@ -1,171 +0,0 @@ -from hyperon import * -from hyperon.ext import register_atoms -import openai -import os -import json -openai.api_key = os.environ["OPENAI_API_KEY"] - -def to_nested_expr(xs): - if isinstance(xs, list): - return E(*list(map(to_nested_expr, xs))) - return ValueAtom(xs) - -def atom2msg(atom): - if isinstance(atom, ExpressionAtom): - # Avoid () in Expression representation - txt = "" - for ch in atom.get_children(): - txt += atom2msg(ch) + " " - return txt[:-1] + "\n" - if isinstance(atom, GroundedAtom): - if isinstance(atom.get_grounded_type(), ExpressionAtom): - return repr(atom) - if isinstance(atom.get_object(), ValueObject): - # Parse String separately to avoid "" in its repr - v = atom.get_object().value - if isinstance(v, str): - return v.replace("\\n", "\n") - return repr(atom) - -def get_llm_args(metta: MeTTa, prompt_space: SpaceRef, *args): - messages = [] - functions = [] - msg_atoms = [] - def __msg_update(m, f, a): - nonlocal messages, functions, msg_atoms - messages += m - functions += f - msg_atoms += [a] - for arg in args: - if isinstance(arg, GroundedAtom) and \ - isinstance(arg.get_object(), SpaceRef): - # FIXME? This will overwrites the current prompt_space if it is set. - # It is convenient to have it here to successfully execute - # (llm &prompt (Functions fn)), when fn is defined in &prompt. - # But (function fn) can also be put in &prompt directly. - # Depending on what is more convenient, this overriding can be changed. - prompt_space = arg.get_object() - __msg_update(*get_llm_args(metta, prompt_space, *prompt_space.get_atoms())) - elif isinstance(arg, ExpressionAtom): - ch = arg.get_children() - if len(ch) > 1: - name = ch[0].get_name() - if name == 'Messages': - __msg_update(*get_llm_args(metta, prompt_space, *ch[1:])) - elif name in ['system', 'user', 'assistant']: - # We have to interpret the message in the main space context, - # if the prompt template is in a separate file and contains - # some external symbols like (user-query) - msg = interpret(metta.space(), ch[1])[0] - messages += [{'role': name, 'content': atom2msg(msg)}] - msg_atoms += [arg] - elif name in ['Functions', 'function']: - for fn in ch[1:]: - doc = None - if prompt_space is not None: - # TODO: Querying for a function description in prompt_space works well, - # but it is useless, because this function cannot be called - # from the main script, so the functional call is not reduced. - # Fixing this requires in general better library management in MeTTa, - # although it can be managed here by interpreting the functional call expression. - # Another approach would be to have load-template, which will import all functions to &self - # (or just to declare function in separate files and load to self, since we may want them - # to be reusable between templates) - r = prompt_space.query(E(S('='), E(S('doc'), fn), V('r'))) - if not r.is_empty(): - doc = r[0]['r'] - if doc is None: - # We use `match` here instead of direct `doc` evaluation - # to evoid non-reduced `doc` - doc = metta.run(f"! (match &self (= (doc {fn}) $r) $r)") - if len(doc) == 0 or len(doc[0]) == 0: - raise RuntimeError(f"No {fn} function description") - doc = doc[0][0] - # TODO: format is not checked - doc = doc.get_children() - properties = {} - for par in doc[2].get_children()[1:]: - p = par.get_children() - properties.update({ - p[0].get_name(): { - "type": "string", - "description": p[1].get_object().value, - "enum": list(map(lambda x: x.get_object().value, p[2].get_children())) - } - }) - functions += [{ - "name": fn.get_name(), - "description": doc[1].get_children()[1].get_object().value, - "parameters": { - "type": "object", - "properties": properties - } - }] - elif name == '=': - # We ignore equalities here: if a space is used to store messages, - # it can contain equalities as well (another approach would be to - # ignore everythins except valid roles) - continue - else: - raise RuntimeError("Unrecognized argument: " + repr(arg)) - else: - # Ignore an empty expression () for convenience, but we need - # to put it back into msg_atoms to keep the structure - msg_atoms += [arg] - else: - raise RuntimeError("Unrecognized argument: " + repr(arg)) - # Do not wrap a single message into Message (necessary to avoid double - # wrapping of single Message argument) - return messages, functions, \ - msg_atoms[0] if len(msg_atoms) == 1 else E(S('Messages'), *msg_atoms) - -def llm(metta: MeTTa, *args): - messages, functions, msgs_atom = get_llm_args(metta, None, *args) - #print(messages) - #return [] - if functions==[]: - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo-0613", - messages=messages, - temperature=0, - timeout = 15) - else: - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo-0613", - messages=messages, - functions=functions, - function_call="auto", - temperature=0, - timeout = 15) - response_message = response["choices"][0]["message"] - #print(response_message) - #messages.append(response_message) - if response_message.get("function_call"): - fs = S(response_message["function_call"]["name"]) - args = response_message["function_call"]["arguments"] - args = json.loads(args) - return [E(fs, to_nested_expr(list(args.values())), msgs_atom)] - return [ValueAtom(response_message['content'])] - -@register_atoms(pass_metta=True) -def llmgate_atoms(metta): - llmAtom = OperationAtom('llm', lambda *args: llm(metta, *args), unwrap=False) - # Just a helper function if one needs to print from a metta-script - # the message converted from expression to text - msgAtom = OperationAtom('atom2msg', - lambda atom: [ValueAtom(atom2msg(atom))], unwrap=False) - return { - r"llm": llmAtom, - r"atom2msg": msgAtom - } - - -def str_find_all(str, values): - return list(filter(lambda v: v in str, values)) - -@register_atoms -def postproc_atoms(): - strfindAtom = OperationAtom('str-find-all', str_find_all) - return { - r"str-find-all": strfindAtom, - } From 594ce6ff79f6fe3ff9d4fe91e921f2db21f21844 Mon Sep 17 00:00:00 2001 From: Vitaly Bogdanov Date: Thu, 16 Nov 2023 14:17:01 +0300 Subject: [PATCH 33/33] Add quote, unify, empty into Rust interpreter's stdlib --- lib/src/metta/runner/stdlib.rs | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/lib/src/metta/runner/stdlib.rs b/lib/src/metta/runner/stdlib.rs index f0be2b468..f57b50c80 100644 --- a/lib/src/metta/runner/stdlib.rs +++ b/lib/src/metta/runner/stdlib.rs @@ -1235,6 +1235,21 @@ pub static METTA_CODE: &'static str = " (= (if True $then $else) $then) (= (if False $then $else) $else) (: Error (-> Atom Atom ErrorType)) + + + ; quote prevents atom from being reduced + (: quote (-> Atom Atom)) + + ; unify matches two atoms and returns $then if they are matched + ; and $else otherwise. + (: unify (-> Atom Atom Atom Atom %Undefined%)) + (= (unify $a $a $then $else) $then) + (= (unify $a $b $then $else) + (case (let (quote $a) (quote $b) no-result) ((%void% $else))) ) + + ; empty removes current result from a non-deterministic result + (: empty (-> %Undefined%)) + (= (empty) (let a b never-happens)) "; #[cfg(all(test, not(feature = "minimal")))] @@ -1637,4 +1652,53 @@ mod tests { fn test_let_op_inside_other_operation() { assert_eq!(run_program("!(and True (let $x False $x))"), Ok(vec![vec![expr!({Bool(false)})]])); } + + #[test] + fn test_quote() { + let metta = Metta::new(Some(EnvBuilder::test_env())); + let parser = SExprParser::new(" + (= (foo) a) + (= (foo) b) + !(foo) + !(quote (foo)) + "); + + assert_eq_metta_results!(metta.run(parser), + Ok(vec![ + vec![expr!("a"), expr!("b")], + vec![expr!("quote" ("foo"))], + ])); + } + + #[test] + fn test_unify() { + let metta = Metta::new(Some(EnvBuilder::test_env())); + let parser = SExprParser::new(" + !(unify (a $b 1 (d)) (a $a 1 (d)) ok nok) + !(unify (a $b c) (a b $c) (ok $b $c) nok) + !(unify $a (a b c) (ok $a) nok) + !(unify (a b c) $a (ok $a) nok) + !(unify (a b c) (a b d) ok nok) + "); + + assert_eq_metta_results!(metta.run(parser), + Ok(vec![ + vec![expr!("ok")], + vec![expr!("ok" "b" "c")], + vec![expr!("ok" ("a" "b" "c"))], + vec![expr!("ok" ("a" "b" "c"))], + vec![expr!("nok")] + ])); + } + + #[test] + fn test_empty() { + let metta = Metta::new(Some(EnvBuilder::test_env())); + let parser = SExprParser::new(" + !(empty) + "); + + assert_eq_metta_results!(metta.run(parser), + Ok(vec![vec![]])); + } }