Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing py-tuple py-list py-dict py-chain #671

Merged
merged 14 commits into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion python/hyperon/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os

from .atoms import ExpressionAtom, E, GroundedAtom, OperationAtom, ValueAtom, NoReduceError, AtomType, MatchableObject, \
G, S, Atoms
G, S, Atoms, ValueObject, OperationObject
from .base import Tokenizer, SExprParser
from .ext import register_atoms, register_tokens
import hyperonpy as hp
Expand Down Expand Up @@ -212,3 +212,52 @@ def load_ascii_atom(space, name):
return {
r"load-ascii": loadAtom
}

def groundedatom_to_python_object(a):
obj = a.get_object()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this function is needed with such implementation. You don't need to check if the type is ValueObject or OperationObject. Both of them have content field (and value or op getter). If there are no other checks, then groundedatom_to_python_object(a) is the same as obj.get_object().content. The latter is even shorter.

if isinstance(obj, ValueObject):
obj = obj.value
if isinstance(obj, OperationObject):
obj = obj.content
return obj

# convert nested tuples to nested python tuples or lists
def _py_tuple_list(tuple_list, *atoms):
rez = []
for a in atoms:
if isinstance(a, GroundedAtom):
rez.append(groundedatom_to_python_object(a))
elif isinstance(a, ExpressionAtom):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add one more
elif isinstance(a, SymbolAtom):
rez.append(get_string_value(a))

or you see some problems with it?

(py-tulple (user "some message)) is not working now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this case it is better to add it directly in groundedatom_to_python_object. @Necr0x0Der What do you think about it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turning arbitrary atoms to Python objects is generally useful. The problem is that there can be some difference how we would like to do this. In particular, in some cases we would like to distinguish, say, S("foo") from ValueAtom("foo") after conversion to Python, while in others we wouldn't want. However, if the main intention is to have an extractor of Python grounded atoms content to Python itself, then we definitely want ValueAtom("foo") to become "foo" rather than "\"foo\"". Thus, if we want to add support for symbols within this functionality, they will be naturally mixed up, which seems ok for this purpose. OTOH, we may want to do it differently, and, say, (py-tulple (user "some message")) could result in tuple (S("user"), "some message"), so the symbol is added to the tuple, but not unwrapped from the atom. From general perspective, I'd prefer the latter behavior, but not sure if it will be convenient.
BTW, if we know that isinstance(a, SymbolAtom), then it is preferable to use a.name rather than get_string_value(a).
One more thing is that if we introduce something like groundedatom_to_python_object and add the case for Symbol there (turning it effectively to atom_to_python), shouldn't we add the case for Expression there as well? Apparently, the issue is that we may want to process expressions for tuples/lists and dicts recursively in different ways. I'd possibly start with the way of how nested lists/dicts (and their mixtures) should be represented in MeTTa (both with symbols, variables, etc.), and then consider better ways to implement this.
The question is also whether we narrowly focus on a convenient way to pass tuples and dicts to native Python functions, or we want to be able to use this in MeTTa itself as well.

rez.append(_py_tuple_list(tuple_list, *a.get_children()))
return tuple_list(rez)

def py_tuple(*atoms):
return [ValueAtom(_py_tuple_list(tuple, *atoms))]

def py_list(*atoms):
return [ValueAtom(_py_tuple_list(list, *atoms))]

def tuple_to_keyvalue(a):
ac = a.get_children()
if len(ac) != 2:
raise Exception("Syntax error in tuple_to_keyvalue")
return groundedatom_to_python_object(ac[0]), groundedatom_to_python_object(ac[1])

# convert pair of tuples to python dictionary
def py_dict(*atoms):
return [ValueAtom(dict([tuple_to_keyvalue(a) for a in atoms]))]

# chain python objects with | (syntactic sugar for langchain)
def py_chain(*atoms):
objects = [groundedatom_to_python_object(a) for a in atoms]
result = objects[0]
for obj in objects[1:]:
result = result | obj
return [ValueAtom(result)]

@register_atoms()
def py_funs():
return {"py-tuple": OperationAtom("py-tuple", py_tuple, unwrap = False),
"py-list": OperationAtom("py-list", py_list, unwrap = False),
"py-dict": OperationAtom("py-dict", py_dict, unwrap = False),
"py-chain": OperationAtom("py-chain", py_chain, unwrap = False)}
11 changes: 5 additions & 6 deletions python/sandbox/simple_import/example_01.metta
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
!(import! &self simple_import)

!(import_from example_01 import simple_fun)
!(import_from example_01 import SimpleObject)
!(bind! simple_fun (py-atom example_01.simple_fun))
!(bind! SimpleObject (py-atom example_01.SimpleObject))

!(bind! so (SimpleObject))


; it is important that obj will have type SimpleObject when passed to simple_fun!
!(simple_fun 1 2 "3" (kwarg1 2) (obj so) )
!(simple_fun 1 2 "3" (Kwargs (kwarg1 2) (obj so)) )

!(call_dot so method "arg1" "arg2" (arg3 3))
!( (py-dot so method) "arg1" "arg2" (Kwargs (arg3 3)) )
24 changes: 11 additions & 13 deletions python/sandbox/simple_import/example_02_numpy.metta
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
!(import! &self simple_import)
!(bind! np (py-atom numpy))

!(import_as numpy as np)
!(bind! a1 ( (py-dot np array) (py-atom (py-tuple 1 2 3) )))
!(bind! a2 ( (py-dot a1 __mul__) 3))
!(bind! a3 ( (py-dot a1 __add__) a2))

!(bind! a1 (call_dot np array (ptuple 1 2 3) ))
!(bind! a2 (call_dot a1 __mul__ 3))
!(bind! a3 (call_dot a1 __add__ a2))

!(a1)
!(a2)
!(a3)

!(__unwrap a1)
!(__unwrap a2)
!(__unwrap a3)
!(bind! m1 ((py-dot np array) (py-atom (py-list (1 2 3) (py-list 4 4 5) (py-tuple 6 7 8)) )))
!(bind! linalg (py-atom numpy.linalg))
!(bind! m1_inv ( (py-dot linalg inv) m1))

!(bind! m1 (call_dot np array (ptuple (1 2 3) (4 4 5) (6 7 8)) ))
!(import_as numpy.linalg as linalg)
!(bind! m1_inv (call_dot linalg inv m1))

!(__unwrap (call_dot np matmul m1 m1_inv))
!( (py-dot np matmul) m1 m1_inv)
23 changes: 10 additions & 13 deletions python/sandbox/simple_import/example_03_langchain.metta
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
!(import! &self simple_import)
!(bind! ChatOpenAI (py-atom langchain_openai.ChatOpenAI))
!(bind! ChatPromptTemplate (py-atom langchain_core.prompts.ChatPromptTemplate))
!(bind! StrOutputParser (py-atom langchain_core.output_parsers.StrOutputParser))

!(import_from langchain_openai import ChatOpenAI)
!(import_from langchain_core.prompts import ChatPromptTemplate)
!(import_from langchain_core.output_parsers import StrOutputParser)
!(bind! model (ChatOpenAI (Kwargs (temperature 0) (model "gpt-3.5-turbo"))))

!(bind! prompt ( (py-dot ChatPromptTemplate from_template) "tell me a joke about cat"))

!(bind! model (ChatOpenAI (temperature 0) (model "gpt-3.5-turbo")))
!(bind! chain1 (py-chain prompt model (StrOutputParser) ))

!(bind! prompt (call_dot ChatPromptTemplate from_template "tell me a joke about cat"))
!( (py-dot chain1 invoke) (py-dict))

!(bind! chain1 (chain prompt model (StrOutputParser) ))
!(bind! prompt2 ( (py-dot ChatPromptTemplate from_messages ) (py-tuple ("system" "You are very funny") ("user" "tell me joke about {foo}"))))

!(__unwrap(call_dot chain1 invoke (pdict)))
!(bind! chain2 (py-chain prompt2 model (StrOutputParser) ))

!(bind! prompt2 (call_dot ChatPromptTemplate from_messages (ptuple ("system" "You are very funny") ("user" "tell me joke about {foo}"))))

!(bind! chain2 (chain prompt2 model (StrOutputParser) ))

!(__unwrap(call_dot chain2 invoke (pdict (foo "dogs") )))
!((py-dot chain2 invoke) (py-dict ("foo" "dogs")))

18 changes: 5 additions & 13 deletions python/sandbox/simple_import/example_04_numpy_simple_import.metta
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
!(import! &self simple_import)
!(bind! linalg (py-atom numpy.linalg))
!(bind! numpy (py-atom numpy))

; with simple "import" it is rather common that we import something
; twice because of submodules in python
; So let's import twice to make sure that it does not cause any problems
!(import numpy)
!(import numpy)
!(import numpy.linalg)


!(bind! m1 (call_dot2 numpy random rand 3 3 ))
!(bind! m1_inv (call_dot2 numpy linalg inv m1))

!(__unwrap (call_dot numpy matmul m1 m1_inv))
!(bind! m1 ((py-dot numpy random.rand) 3 3 ))
!(bind! m1_inv ( (py-dot linalg inv) m1))
!( (py-dot numpy matmul) m1 m1_inv)
147 changes: 0 additions & 147 deletions python/sandbox/simple_import/simple_import.py

This file was deleted.

Loading