Skip to content

Commit

Permalink
Merge branch 'main' into no-module-duplication
Browse files Browse the repository at this point in the history
  • Loading branch information
vsbogd authored Oct 22, 2024
2 parents ab9c0c1 + 021a037 commit f1432b1
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ edition = "2021"

[workspace.dependencies]
hyperon = { path = "./lib", version = "0.2.0" }
regex = "1.5.4"
regex = "1.11.0"
log = "0.4.0"
env_logger = "0.8.4"

Expand Down
22 changes: 21 additions & 1 deletion lib/src/metta/runner/stdlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1872,7 +1872,7 @@ mod non_minimal_only_stdlib {
|token| { Ok(Atom::gnd(Number::from_float_str(token)?)) });
tref.register_token(regex(r"True|False"),
|token| { Atom::gnd(Bool::from_str(token)) });
tref.register_token(regex(r#""[^"]+""#),
tref.register_token(regex(r#"^".*"$"#),
|token| { let mut s = String::from(token); s.remove(0); s.pop(); Atom::gnd(Str::from_string(s)) });
let sum_op = Atom::gnd(SumOp{});
tref.register_token(regex(r"\+"), move |_| { sum_op.clone() });
Expand Down Expand Up @@ -1969,6 +1969,7 @@ mod tests {
use crate::atom::matcher::atoms_are_equivalent;
use crate::metta::text::*;
use crate::metta::runner::EnvBuilder;
use crate::metta::runner::string::Str;
use crate::metta::types::validate_atom;
use crate::common::test_utils::*;

Expand Down Expand Up @@ -2714,4 +2715,23 @@ mod tests {
("@return" ("@type" "%Undefined%") ("@desc" {Str::from_str("Return value")})) )],
]));
}

#[test]
fn test_string_parsing() {
let metta = Metta::new(Some(EnvBuilder::test_env()));
let parser = SExprParser::new(r#"
(= (id $x) $x)
!(id "test")
!(id "te st")
!(id "te\"st")
!(id "")
"#);

assert_eq_metta_results!(metta.run(parser), Ok(vec![
vec![expr!({Str::from_str("test")})],
vec![expr!({Str::from_str("te st")})],
vec![expr!({Str::from_str("te\"st")})],
vec![expr!({Str::from_str("")})],
]));
}
}
4 changes: 2 additions & 2 deletions lib/src/metta/runner/stdlib_minimal.metta
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@
(: decons-atom (-> Expression Expression))

(@doc collapse-bind
(@desc "Evaluates the Atom (first argument) and returns an expression which contains all alternative evaluations in a form (Atom Bindings). Bindings are represented in a form of a grounded atom.")
(@desc "Evaluates minimal MeTTa operation (first argument) and returns an expression which contains all alternative evaluations in a form (Atom Bindings). Bindings are represented in a form of a grounded atom.")
(@params (
(@param "Atom to be evaluated")))
(@param "Minimal MeTTa operation to be evaluated")))
(@return "All alternative evaluations"))
(: collapse-bind (-> Atom Expression))

Expand Down
25 changes: 24 additions & 1 deletion lib/src/metta/runner/stdlib_minimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ pub fn register_rust_stdlib_tokens(target: &mut Tokenizer) {
|token| { Ok(Atom::gnd(Number::from_float_str(token)?)) });
tref.register_token(regex(r"True|False"),
|token| { Atom::gnd(Bool::from_str(token)) });
tref.register_token(regex(r#""[^"]+""#),
tref.register_token(regex(r#"(?s)^".*"$"#),
|token| { let mut s = String::from(token); s.remove(0); s.pop(); Atom::gnd(Str::from_string(s)) });
let sum_op = Atom::gnd(SumOp{});
tref.register_token(regex(r"\+"), move |_| { sum_op.clone() });
Expand Down Expand Up @@ -550,6 +550,7 @@ mod tests {
use super::*;
use crate::metta::text::SExprParser;
use crate::metta::runner::EnvBuilder;
use crate::metta::runner::string::Str;
use crate::matcher::atoms_are_equivalent;
use crate::common::Operation;
use crate::common::test_utils::metta_space;
Expand Down Expand Up @@ -1275,4 +1276,26 @@ mod tests {
vec![expr!("Error" ({SumOp{}} {Number::Integer(1)} {Number::Integer(2)}) ({SumOp{}} {Number::Integer(1)} {SumOp{}}))],
]));
}

#[test]
fn test_string_parsing() {
let metta = Metta::new(Some(EnvBuilder::test_env()));
let parser = SExprParser::new(r#"
!(id "test")
!(id "te st")
!(id "te\"st")
!(id "")
!(id "te\nst")
!("te\nst"test)
"#);

assert_eq_metta_results!(metta.run(parser), Ok(vec![
vec![expr!({Str::from_str("test")})],
vec![expr!({Str::from_str("te st")})],
vec![expr!({Str::from_str("te\"st")})],
vec![expr!({Str::from_str("")})],
vec![expr!({Str::from_str("te\nst")})],
vec![expr!({Str::from_str("te\nst")} "test")],
]));
}
}
60 changes: 33 additions & 27 deletions python/hyperon/atoms.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,35 @@ class MettaError(Exception):
, but we don't want to output Python error stack."""
pass

def unwrap_args(atoms):
args = []
kwargs = {}
for a in atoms:
if isinstance(a, ExpressionAtom):
ch = a.get_children()
if len(ch) > 0 and repr(ch[0]) == "Kwargs":
for c in ch[1:]:
try:
kwarg = c.get_children()
assert len(kwarg) == 2
except:
raise RuntimeError(f"Incorrect kwarg format {kwarg}")
try:
kwargs[get_string_value(kwarg[0])] = kwarg[1].get_object().content
except:
raise NoReduceError()
continue
try:
args.append(a.get_object().content)
except:
# NOTE:
# Currently, applying grounded operations to pure atoms is not reduced.
# If we want, we can raise an exception, or form an error expression instead,
# so a MeTTa program can catch and analyze it.
# raise RuntimeError("Grounded operation " + self.name + " with unwrap=True expects only grounded arguments")
raise NoReduceError()
return args, kwargs

class OperationObject(GroundedObject):
"""
An OperationObject represents an operation as a grounded object, allowing for more
Expand Down Expand Up @@ -385,32 +414,7 @@ def execute(self, *atoms, res_typ=AtomType.UNDEFINED):
"""
# type-check?
if self.unwrap:
args = []
kwargs = {}
for a in atoms:
if isinstance(a, ExpressionAtom):
ch = a.get_children()
if len(ch) > 0 and repr(ch[0]) == "Kwargs":
for c in ch[1:]:
try:
kwarg = c.get_children()
assert len(kwarg) == 2
except:
raise RuntimeError(f"Incorrect kwarg format {kwarg}")
try:
kwargs[get_string_value(kwarg[0])] = kwarg[1].get_object().content
except:
raise NoReduceError()
continue
try:
args.append(a.get_object().content)
except:
# NOTE:
# Currently, applying grounded operations to pure atoms is not reduced.
# If we want, we can raise an exception, or form an error expression instead,
# so a MeTTa program can catch and analyze it.
# raise RuntimeError("Grounded operation " + self.name + " with unwrap=True expects only grounded arguments")
raise NoReduceError()
args, kwargs = unwrap_args(atoms)
try:
result = self.op(*args, **kwargs)
except MettaError as e:
Expand All @@ -422,7 +426,9 @@ def execute(self, *atoms, res_typ=AtomType.UNDEFINED):
return [ValueAtom(result, res_typ)]
else:
result = self.op(*atoms)
if not isinstance(result, list):
try:
iter(result)
except TypeError:
raise RuntimeError("Grounded operation `" + self.name + "` should return list")
return result

Expand Down
2 changes: 2 additions & 0 deletions python/hyperon/exts/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .agent_base import AgentObject
from .agent_base import agent_atoms
174 changes: 174 additions & 0 deletions python/hyperon/exts/agents/agent_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from hyperon import *
from hyperon.ext import register_atoms

'''
This is very preliminary and incomplete PoC version.
However, it is put to exts, because metta-motto depends on it.
Reagrding threading:
- Generic threading for metta can be introduced with
parallel and sequential composition, for-comprehension, etc.
Agents could be built on top of this functionality. However,
this piece of code was driven by metta-motto demands.
- Two main cases for agents are:
-- Immediate call with inputs to get outputs
-- Asynchronous events and responses
Supporting both cases in one implementation is more convenient,
because both of them can be needed simultaneously in certain
domains (e.g. metta-motto)
- Implementation can be quite different.
-- Agents could be started explicitly
-- They could inherint from StreamMethod
-- Other methods could be called directly without StreamMethod wrapper
All these nuances are to be fleshed out
'''

import threading
from queue import Queue
class StreamMethod(threading.Thread):
def __init__(self, method, args):
super().__init__() #daemon=True
self._result = Queue()
self.method = method
self.args = args
def run(self):
for r in self.method(*self.args):
self._result.put(r)
def __iter__(self):
return self
def __next__(self):
if self._result.empty() and not self.is_alive():
raise StopIteration
return self._result.get()

class AgentObject:

'''
The base agent object class, which purpose is twofold.
1) It allows using Metta scripts as agents.
2) Python classes inherited from it, can be used for
creating agents in metta (almost) without additional
boilerplate code.
These two purposes are combined in one base class, so one
can inherit from it and add functionality, which will be
shared between Python and Metta agents (as how it happens
in Metta-motto)
'''
_name = None

@classmethod
def get_agent_atom(cls, metta, *args, unwrap=True):
# metta and unwrap are not passed to __init__, because
# they are needed only for __metta_call__, so children
# classes do not need to pass them to super().__init__
if unwrap:
# a hacky way to unwrap args
agent_atom = OperationObject("_", cls).execute(*args)[0]
agent = agent_atom.get_object().content
else:
agent = cls(*args)
if metta is not None:
if hasattr(agent, '_metta') and agent._metta is not None:
raise RuntimeError(f"MeTTa is already defined for {agent}")
agent._metta = metta
agent._unwrap = unwrap
return [OperationAtom(cls.name(),
lambda *agent_args: agent.__metta_call__(*agent_args), unwrap=False)]

@classmethod
def agent_creator_atom(cls, metta=None, unwrap=True):
return OperationAtom(cls.name()+'-agent',
lambda *args: cls.get_agent_atom(metta, *args, unwrap=unwrap),
unwrap=False)

@classmethod
def name(cls):
return cls._name if cls._name is not None else str(cls)

def _try_unwrap(self, val):
if val is None or isinstance(val, str):
return val
if isinstance(val, GroundedAtom):
return str(val.get_object().content)
return repr(val)

def __init__(self, path=None, atoms={}, include_paths=None, code=None):
if path is None and code is None:
# purely Python agent
return
# The first argument is either path or code when called from MeTTa
if isinstance(path, ExpressionAtom):# and path != E():
code = path
elif path is not None:
path = self._try_unwrap(path)
with open(path, mode='r') as f:
code = f.read()
# _code can remain None if the agent uses parent runner (when called from MeTTa)
self._code = code.get_children()[1] if isinstance(code, ExpressionAtom) else \
self._try_unwrap(code)
self._atoms = atoms
self._include_paths = include_paths
self._context_space = None
self._create_metta()

def _create_metta(self):
if self._code is None:
return None
self._init_metta()
self._load_code() # TODO: check that the result contains only units

def _init_metta(self):
### =========== Creating MeTTa runner ===========
# NOTE: each MeTTa agent uses its own space and runner,
# which are not inherited from the caller agent. Thus,
# the caller space is not directly accessible as a context,
# except the case when _metta is set via get_agent_atom with parent MeTTa
if self._include_paths is not None:
env_builder = Environment.custom_env(include_paths=self._include_paths)
metta = MeTTa(env_builder=env_builder)
else:
metta = MeTTa()
# Externally passed atoms for registrations
for k, v in self._atoms.items():
metta.register_atom(k, v)
self._metta = metta

def _load_code(self):
return self._metta.run(self._code) if isinstance(self._code, str) else \
self._metta.space().add_atom(self._code)

def __call__(self, atom):
if self._unwrap or self._metta is None:
raise NotImplementedError(
f"__call__ for {self.__class__.__name__} should be defined"
)
return self._metta.evaluate_atom(atom)

def is_daemon(self):
return hasattr(self, 'daemon') and self.daemon is True

def __metta_call__(self, *args):
call = True
method = self.__call__
if len(args) > 0 and isinstance(args[0], SymbolAtom):
n = args[0].get_name()
if n[0] == '.' and hasattr(self, n[1:]):
method = getattr(self, n[1:])
args = args[1:]
call = False
if self._unwrap:
method = OperationObject(f"{method}", method).execute
st = StreamMethod(method, args)
st.start()
# We don't return the stream here; otherwise it will be consumed immediately.
# If the agent itself would be StreamMethod, its results could be accessbile.
# Here, they are lost (TODO?).
if call and self.is_daemon():
return [E()]
return st


@register_atoms(pass_metta=True)
def agent_atoms(metta):
return {
r"create-agent": AgentObject.agent_creator_atom(unwrap=False),
}
2 changes: 2 additions & 0 deletions python/hyperon/exts/agents/tests/agent.metta
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(= (g $t) (h $t $t))
(= (h $a $b) (* $a $b))
5 changes: 5 additions & 0 deletions python/hyperon/exts/agents/tests/test_agents.metta
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
!(import! &self agents)

!((create-agent agent.metta) (g 3)) ; 9

!((create-agent (quote (= (f $x) (+ 1 $x)))) (f 10)) ; 11
Loading

0 comments on commit f1432b1

Please sign in to comment.