bryan-stearns
commited on
Commit
•
2130399
1
Parent(s):
e83fdc6
Adding py files
Browse files- .gitattributes +2 -0
- Dockerfile +20 -0
- README.md +3 -3
- gradio_app.py +45 -0
- pysoarlib/AgentConnector.py +88 -0
- pysoarlib/IdentifierExtensions.py +79 -0
- pysoarlib/LICENSE +21 -0
- pysoarlib/README.md +285 -0
- pysoarlib/SVSCommands.py +84 -0
- pysoarlib/SoarClient.py +375 -0
- pysoarlib/SoarWME.py +87 -0
- pysoarlib/TimeConnector.py +181 -0
- pysoarlib/WMInterface.py +53 -0
- pysoarlib/__init__.py +36 -0
- pysoarlib/__pycache__/__init__.cpython-310.pyc +0 -0
- pysoarlib/util/PrintoutIdentifier.py +76 -0
- pysoarlib/util/WMNode.py +113 -0
- pysoarlib/util/__init__.py +8 -0
- pysoarlib/util/extract_wm_graph.py +33 -0
- pysoarlib/util/parse_wm_printout.py +75 -0
- pysoarlib/util/remove_tree_from_wm.py +18 -0
- pysoarlib/util/update_wm_from_tree.py +35 -0
- requirements.txt +2 -0
- soar_io.py +35 -0
.gitattributes
CHANGED
@@ -6,6 +6,7 @@
|
|
6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
|
|
9 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
@@ -23,6 +24,7 @@
|
|
23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
|
|
26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
|
|
6 |
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
+
*.jar filter=lfs diff=lfs merge=lfs -text
|
10 |
*.joblib filter=lfs diff=lfs merge=lfs -text
|
11 |
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
12 |
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
|
|
24 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
25 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
26 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
27 |
+
*.so filter=lfs diff=lfs merge=lfs -text
|
28 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
29 |
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
30 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.10-slim-buster
|
2 |
+
|
3 |
+
WORKDIR /app
|
4 |
+
|
5 |
+
COPY requirements.txt requirements.txt
|
6 |
+
RUN pip3 install -r requirements.txt
|
7 |
+
|
8 |
+
ENV SOAR_HOME="/app/soar-bin_debian-buster"
|
9 |
+
ENV PYTHONPATH="${SOAR_HOME}:${PYTHONPATH}"
|
10 |
+
ENV DYLD_LIBRARY_PATH="${SOAR_HOME}:${DYLD_LIBRARY_PATH}"
|
11 |
+
|
12 |
+
COPY . .
|
13 |
+
|
14 |
+
RUN chmod 777 /app/*
|
15 |
+
# RUN chmod 777 /app/data/*
|
16 |
+
|
17 |
+
EXPOSE 7860 7860
|
18 |
+
|
19 |
+
ENTRYPOINT ["python3"]
|
20 |
+
CMD [ "gradio_app.py"]
|
README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
---
|
2 |
title: Soar
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: bsd
|
|
|
1 |
---
|
2 |
title: Soar
|
3 |
+
emoji: 🦅
|
4 |
+
colorFrom: white
|
5 |
+
colorTo: blue
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: bsd
|
gradio_app.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
import gradio as gr
|
3 |
+
|
4 |
+
from pysoarlib import SoarClient
|
5 |
+
from soar_io import GeneralSoarIOConnector
|
6 |
+
|
7 |
+
class SoarAgent():
|
8 |
+
def __init__(self):
|
9 |
+
self.client, self.agent_io = self.make_soar_agent()
|
10 |
+
print("Agent created.")
|
11 |
+
|
12 |
+
def make_soar_agent(self):
|
13 |
+
client = SoarClient(write_to_stdout=True,remote_connection=False)
|
14 |
+
myIOConnector = GeneralSoarIOConnector(client)
|
15 |
+
client.add_connector("generalIO", myIOConnector)
|
16 |
+
client.connect()
|
17 |
+
|
18 |
+
return client, myIOConnector
|
19 |
+
|
20 |
+
def execute_command(self, cmd):
|
21 |
+
try:
|
22 |
+
self.client.execute_command(str(cmd))
|
23 |
+
except:
|
24 |
+
print(" * ERROR: Caught exception from Soar", file=sys.stderr)
|
25 |
+
|
26 |
+
def get_output(self):
|
27 |
+
return self.agent_io.get_agent_output()
|
28 |
+
|
29 |
+
def kill(self):
|
30 |
+
self.client.kill()
|
31 |
+
print("Agent shut down.")
|
32 |
+
|
33 |
+
|
34 |
+
soar_agent = SoarAgent()
|
35 |
+
|
36 |
+
def run_soar(cmd):
|
37 |
+
soar_agent.execute_command(cmd)
|
38 |
+
agent_output = soar_agent.get_output()
|
39 |
+
return str(agent_output)
|
40 |
+
|
41 |
+
gr.Interface(fn=run_soar,
|
42 |
+
inputs=["text"],
|
43 |
+
outputs=["text"]).launch(inbrowser=True, server_port=7860)
|
44 |
+
|
45 |
+
soar_agent.kill()
|
pysoarlib/AgentConnector.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
""" Generic Base Class for interfacing with a soar agent's input/output links
|
2 |
+
|
3 |
+
A Connector can be added to a SoarClient and can handle input/output
|
4 |
+
while taking care of specific SML calls and event registering
|
5 |
+
"""
|
6 |
+
|
7 |
+
from __future__ import print_function
|
8 |
+
|
9 |
+
import traceback, sys
|
10 |
+
|
11 |
+
class AgentConnector(object):
|
12 |
+
""" Base Class for handling input/output for a soar agent
|
13 |
+
|
14 |
+
Input:
|
15 |
+
on_input_phase will be automatically called before each input phase
|
16 |
+
|
17 |
+
Output:
|
18 |
+
call add_output_command to add the name of an output-link command to look for
|
19 |
+
on_output_event will then be called if such a command is added by the agent
|
20 |
+
|
21 |
+
Look at LanguageConnector for an example of an AgentConnector used in practice
|
22 |
+
"""
|
23 |
+
def __init__(self, client):
|
24 |
+
""" Initialize the Connector (but won't register event handlers until connect)
|
25 |
+
|
26 |
+
client should be an instance of SoarClient
|
27 |
+
"""
|
28 |
+
self.client = client
|
29 |
+
self.connected = False
|
30 |
+
self.output_handler_ids = { }
|
31 |
+
|
32 |
+
|
33 |
+
def add_output_command(self, command_name):
|
34 |
+
""" Will cause the connector to handle commands with the given name on the output-link """
|
35 |
+
if self.connected:
|
36 |
+
self.output_handler_ids[command_name] = self.client.agent.AddOutputHandler(
|
37 |
+
command_name, AgentConnector._output_event_handler, self)
|
38 |
+
else:
|
39 |
+
self.output_handler_ids[command_name] = -1
|
40 |
+
|
41 |
+
def connect(self):
|
42 |
+
""" Adds event handlers, automatically called by the SoarClient """
|
43 |
+
if self.connected:
|
44 |
+
return
|
45 |
+
|
46 |
+
for command_name in self.output_handler_ids:
|
47 |
+
self.output_handler_ids[command_name] = self.client.agent.AddOutputHandler(
|
48 |
+
command_name, AgentConnector._output_event_handler, self)
|
49 |
+
|
50 |
+
self.connected = True
|
51 |
+
|
52 |
+
def disconnect(self):
|
53 |
+
""" Removes event handlers, automatically called by the SoarClient """
|
54 |
+
if not self.connected:
|
55 |
+
return
|
56 |
+
|
57 |
+
for command_name in self.output_handler_ids:
|
58 |
+
self.client.agent.RemoveOutputHandler(self.output_handler_ids[command_name])
|
59 |
+
self.output_handler_ids[command_name] = -1
|
60 |
+
|
61 |
+
self.connected = False
|
62 |
+
|
63 |
+
def on_init_soar(self):
|
64 |
+
""" Override to handle an init-soar event (remove references to SML objects """
|
65 |
+
pass
|
66 |
+
|
67 |
+
def on_input_phase(self, input_link):
|
68 |
+
""" Override to update working memory, automatically called before each input phase """
|
69 |
+
pass
|
70 |
+
|
71 |
+
def on_output_event(self, command_name, root_id):
|
72 |
+
""" Override to handle output commands with the given name (added by add_output_command)
|
73 |
+
|
74 |
+
root_id is the root Identifier of the command (e.g. (<output-link> ^command_name <root_id>)
|
75 |
+
"""
|
76 |
+
pass
|
77 |
+
|
78 |
+
@staticmethod
|
79 |
+
def _output_event_handler(self, agent_name, att_name, wme):
|
80 |
+
""" OutputHandler callback for when a command is put on the output link """
|
81 |
+
try:
|
82 |
+
if wme.IsJustAdded() and wme.IsIdentifier():
|
83 |
+
root_id = wme.ConvertToIdentifier()
|
84 |
+
self.on_output_event(att_name, root_id)
|
85 |
+
except:
|
86 |
+
self.client.print_handler("ERROR IN OUTPUT EVENT HANDLER")
|
87 |
+
self.client.print_handler(traceback.format_exc())
|
88 |
+
self.client.print_handler("--------- END ---------------")
|
pysoarlib/IdentifierExtensions.py
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
""" Defines additional helper methods for the Identifier class for accessing child values
|
2 |
+
|
3 |
+
This module is not intended to be imported directly,
|
4 |
+
Importing the pysoarlib module will cause these to be added to the Identifier class
|
5 |
+
Note that the methods will use CamelCase, so get_child_str => GetChildStr
|
6 |
+
"""
|
7 |
+
_INTEGER_VAL = "int"
|
8 |
+
_FLOAT_VAL = "double"
|
9 |
+
_STRING_VAL = "string"
|
10 |
+
|
11 |
+
def get_child_str(self, attribute):
|
12 |
+
""" Given id and attribute, returns value for WME as string (self ^attribute value) """
|
13 |
+
wme = self.FindByAttribute(attribute, 0)
|
14 |
+
if wme == None or len(wme.GetValueAsString()) == 0:
|
15 |
+
return None
|
16 |
+
return wme.GetValueAsString()
|
17 |
+
|
18 |
+
def get_child_int(self, attribute):
|
19 |
+
""" Given id and attribute, returns integer value for WME (self ^attribute value) """
|
20 |
+
wme = self.FindByAttribute(attribute, 0)
|
21 |
+
if wme == None or wme.GetValueType() != _INTEGER_VAL:
|
22 |
+
return None
|
23 |
+
return wme.ConvertToIntElement().GetValue()
|
24 |
+
|
25 |
+
def get_child_float(self, attribute):
|
26 |
+
""" Given id and attribute, returns float value for WME (self ^attribute value) """
|
27 |
+
wme = self.FindByAttribute(attribute, 0)
|
28 |
+
if wme == None or wme.GetValueType() != _FLOAT_VAL:
|
29 |
+
return None
|
30 |
+
return wme.ConvertToFloatElement().GetValue()
|
31 |
+
|
32 |
+
def get_child_id(self, attribute):
|
33 |
+
""" Given id and attribute, returns identifier value of WME (self ^attribute child_id) """
|
34 |
+
wme = self.FindByAttribute(attribute, 0)
|
35 |
+
if wme == None or not wme.IsIdentifier():
|
36 |
+
return None
|
37 |
+
return wme.ConvertToIdentifier()
|
38 |
+
|
39 |
+
def get_all_child_ids(self, attribute=None):
|
40 |
+
""" Given id and attribute, returns a list of child identifiers from all WME's matching (self ^attribute child_id)
|
41 |
+
|
42 |
+
If no attribute is specified, all child identifiers are returned
|
43 |
+
"""
|
44 |
+
child_ids = []
|
45 |
+
for index in range(self.GetNumberChildren()):
|
46 |
+
wme = self.GetChild(index)
|
47 |
+
if not wme.IsIdentifier():
|
48 |
+
continue
|
49 |
+
if attribute == None or wme.GetAttribute() == attribute:
|
50 |
+
child_ids.append(wme.ConvertToIdentifier())
|
51 |
+
return child_ids
|
52 |
+
|
53 |
+
def get_all_child_values(self, attribute=None):
|
54 |
+
""" Given id and attribute, returns a list of strings of non-identifier values from all WME's matching (self ^attribute value)
|
55 |
+
|
56 |
+
If no attribute is specified, all child values (non-identifiers) are returned
|
57 |
+
"""
|
58 |
+
child_values = []
|
59 |
+
for index in range(self.GetNumberChildren()):
|
60 |
+
wme = self.GetChild(index)
|
61 |
+
if wme.IsIdentifier():
|
62 |
+
continue
|
63 |
+
if attribute == None or wme.GetAttribute() == attribute:
|
64 |
+
child_values.append(wme.GetValueAsString())
|
65 |
+
return child_values
|
66 |
+
|
67 |
+
def get_all_child_wmes(self):
|
68 |
+
""" Returns a list of (attr, val) tuples representing all wmes rooted at this identifier
|
69 |
+
val will either be an Identifier or a string, depending on its type """
|
70 |
+
wmes = []
|
71 |
+
for index in range(self.GetNumberChildren()):
|
72 |
+
wme = self.GetChild(index)
|
73 |
+
if wme.IsIdentifier():
|
74 |
+
wmes.append( (wme.GetAttribute(), wme.ConvertToIdentifier()) )
|
75 |
+
else:
|
76 |
+
wmes.append( (wme.GetAttribute(), wme.GetValueAsString()) )
|
77 |
+
return wmes
|
78 |
+
|
79 |
+
|
pysoarlib/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2018 Aaron Mininger
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
pysoarlib/README.md
ADDED
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# PySoarLib
|
2 |
+
## Aaron Mininger
|
3 |
+
### 2018
|
4 |
+
|
5 |
+
This is a python library module with code to help make working with Soar SML in python
|
6 |
+
just a little bit easier.
|
7 |
+
|
8 |
+
Methods do have docstrings, to read help information open a python shell and type
|
9 |
+
```
|
10 |
+
import pysoarlib
|
11 |
+
help(pysoarlib)
|
12 |
+
help(pysoarlib.SoarClient)
|
13 |
+
```
|
14 |
+
|
15 |
+
* [SoarClient](#soarclient)
|
16 |
+
* [Config Settings](#configsettings)
|
17 |
+
* [AgentConnector](#agentconnector)
|
18 |
+
* [IdentifierExtensions](#idextensions)
|
19 |
+
* [WMInterface](#wminterface)
|
20 |
+
* [SoarWME](#soarwme)
|
21 |
+
* [SVSCommands](#svscommands)
|
22 |
+
* [TimeConnector](#timeconnector)
|
23 |
+
* [util](#util)
|
24 |
+
|
25 |
+
<a name="soarclient"></a>
|
26 |
+
# SoarClient
|
27 |
+
Defines a class used for creating a soar agent, sending commands, running it, etc.
|
28 |
+
There are a number of settings that you can use to configure the client (see following subsection) and
|
29 |
+
can specify either by including them in the config file or by giving as keyword arguments
|
30 |
+
in the constructor.
|
31 |
+
|
32 |
+
`SoarClient(config_filename=None, print_handler=None, **kwargs)`
|
33 |
+
Will create the soar kernel and agent, as well as source the agent files.
|
34 |
+
`print_handler` specifies how output is handled (defaults to python print) and
|
35 |
+
`config_filename` names a file with client settings in it
|
36 |
+
|
37 |
+
|
38 |
+
`add_connector(AgentConnector, name:str)`
|
39 |
+
Adds the given connector and will invoke callbacks on it (such as on_input_phase)
|
40 |
+
|
41 |
+
`get_connector(name:str)`
|
42 |
+
Returns a connector of the given name, or None
|
43 |
+
|
44 |
+
`has_connector(name:str) -> Boolean`
|
45 |
+
Returns true if the given connector exists
|
46 |
+
|
47 |
+
`add_print_event_handler(handler)`
|
48 |
+
Will call the given handler during each soar print event (handler should be a method taking 1 string)
|
49 |
+
|
50 |
+
`connect()`
|
51 |
+
Will register callbacks (call before running)
|
52 |
+
|
53 |
+
`disconnect()`
|
54 |
+
Will deregister callbacks
|
55 |
+
|
56 |
+
`start()`
|
57 |
+
Will cause the agent to start running in new thread (non-blocking)
|
58 |
+
|
59 |
+
`stop()`
|
60 |
+
Will stop the agent
|
61 |
+
|
62 |
+
`execute_command(cmd:str, print_res:bool=False)`
|
63 |
+
Sends the given command to the agent and returns the result as a string. If print_res=True it also prints the output using print_handler
|
64 |
+
|
65 |
+
`restart()`
|
66 |
+
Completely destroys the agent and creates + sources a new one
|
67 |
+
|
68 |
+
`kill()`
|
69 |
+
Will stop the agent and destroy the agent/kernel
|
70 |
+
|
71 |
+
|
72 |
+
## Config Settings (kwargs or config file)
|
73 |
+
<a name="configsettings"></a>
|
74 |
+
|
75 |
+
These can be passed as keyword arguments to the SoarClient constructor,
|
76 |
+
or you can create a config file that contains these settings.
|
77 |
+
|
78 |
+
| Argument | Type | Default | Description |
|
79 |
+
| ------------------ | -------- | ---------- | ---------------------------------------- |
|
80 |
+
| `agent_name` | str | soaragent | The soar agent's name |
|
81 |
+
| `agent_source` | filename | | The root soar file to source the agent productions |
|
82 |
+
| `smem_source` | filename | | The root soar file that sources smem add commands |
|
83 |
+
| `source_output` | enum str | summary | How much detail to print when sourcing files: none, summary, or full |
|
84 |
+
| `watch_level` | int | 1 | Sets the soar watch/trace level, how much to print each DC (0=none) |
|
85 |
+
| `spawn_debugger` | bool | false | If true, spawns the soar java debugger |
|
86 |
+
| `start_running` | bool | false | If true, will automatically start running the agent |
|
87 |
+
| `write_to_stdout` | bool | false | If true, will print all soar output to the print_handler |
|
88 |
+
| `print_handler` | method | print | A method taking 1 string arg, handles agent output |
|
89 |
+
| `enable_log` | bool | false | If true, writes all soar/agent output to a file |
|
90 |
+
| `log_filename` | filename | agent-log.txt | The name of the log file to create |
|
91 |
+
| **time settings** <a name="timesettings"></a> | | | |
|
92 |
+
| `use_time_connector`| bool | false | If true, creates a TimeConnector to put time info on the input-link |
|
93 |
+
| `clock_include_ms` | bool | true | Will include milliseconds for elapsed and clock times |
|
94 |
+
| `sim_clock` | bool | false | If false, the clock shows real time. If true, it advances a fixed amount each DC |
|
95 |
+
| `clock_step_ms` | int | 50 | The number of milliseconds the simulated clock advances each decision cycle |
|
96 |
+
|
97 |
+
Instead of passing as arguments, you can include them in a file specified by config_filename
|
98 |
+
Each line in the file should be 'setting = value'
|
99 |
+
|
100 |
+
Example File:
|
101 |
+
```
|
102 |
+
agent_name = Rosie
|
103 |
+
agent_source = agent.soar
|
104 |
+
spawn_debugger = false
|
105 |
+
```
|
106 |
+
|
107 |
+
|
108 |
+
<a name="agentconnector"></a>
|
109 |
+
# AgentConnector
|
110 |
+
Defines an abstract base class for creating classes that connect to Soar's input/output links
|
111 |
+
|
112 |
+
`AgentConnector(client:SoarClient)`
|
113 |
+
|
114 |
+
|
115 |
+
`add_output_command(command_name:str)`
|
116 |
+
Will register a handler that listens to output link commands with the given name
|
117 |
+
|
118 |
+
`add_print_event_handler(handler:func)`
|
119 |
+
Will register a print event handler (function taking 1 string argument) that will be called whenever a soar print event occurs.
|
120 |
+
|
121 |
+
`on_init_soar()`
|
122 |
+
Event Handler called when init-soar happens (need to release SML working memory objects)
|
123 |
+
|
124 |
+
`on_input_phase(input_link:Identifier)`
|
125 |
+
Event Handler called every input phase
|
126 |
+
|
127 |
+
`on_output_event(command_name, root_id)`
|
128 |
+
Event Handler called when a new output link command is created `(<output-link> ^command_name <root_id>)`
|
129 |
+
|
130 |
+
|
131 |
+
|
132 |
+
|
133 |
+
|
134 |
+
<a name="idextensions"></a>
|
135 |
+
# IdentifierExtensions
|
136 |
+
These add a few helper methods to the sml.Identifier class:
|
137 |
+
(Do not need to import directly, come with any import from module)
|
138 |
+
|
139 |
+
`Identifier.GetChildString(attribute:str)`
|
140 |
+
Given an attribute, will look for a child WME of the form `(<id> ^attribute <value>)` and return the value as a string
|
141 |
+
|
142 |
+
`Identifier.GetChildInt(attribute:str)`
|
143 |
+
Given an attribute, will look for a child IntegerWME of the form `(<id> ^attribute <value>)` and return the value as an integer
|
144 |
+
|
145 |
+
`Identifier.GetChildFloat(attribute:str)`
|
146 |
+
Given an attribute, will look for a child FloatWME of the form `(<id> ^attribute <value>)` and return the value as an float
|
147 |
+
|
148 |
+
`Identifier.GetChildId(attribute:str)`
|
149 |
+
Given an attribute, will look for a child WME of the form `(<id> ^attribute <child_id>)` and return an Identifier with child_id as the root
|
150 |
+
|
151 |
+
`Identifier.GetAllChildIds(attribute:str=None)`
|
152 |
+
Given an attribute, returns a list of Identifiers from all child WME's matching `(<id> ^attribute <child_id>)`
|
153 |
+
If no attribute is specified, all child Identifiers are returned
|
154 |
+
|
155 |
+
`Identifier.GetAllChildValues(attribute:str=None)`
|
156 |
+
Given an attribute, returns a list of strings from all child WME's matching `(<id> ^attribute <value>)`
|
157 |
+
If no attribute is specified, all child WME values (non-identifiers) are returned
|
158 |
+
|
159 |
+
`Identifier.GetAllChildWmes()`
|
160 |
+
Returns a list of (attr, val) tuples representing all wmes rooted at this identifier.
|
161 |
+
val will either be an Identifier or a string, depending on its type """
|
162 |
+
|
163 |
+
|
164 |
+
<a name="wminterface"></a>
|
165 |
+
# WMInterface:
|
166 |
+
An interface class which defines a standard way of adding/removing structures from working memory:
|
167 |
+
|
168 |
+
`is_added()`
|
169 |
+
Returns True if the structure is currently added to working memory
|
170 |
+
|
171 |
+
`add_to_wm(parent_id)`
|
172 |
+
Adds the structure to working memory under the given identifier
|
173 |
+
|
174 |
+
`update_wm(parent_id=None)`
|
175 |
+
Applies any changes to working memory
|
176 |
+
Note, if a `parent_id` is given and the item is not yet added to wm, it will add it
|
177 |
+
|
178 |
+
`remove_from_wm()`
|
179 |
+
Removes the structure from working memory
|
180 |
+
|
181 |
+
|
182 |
+
<a name="soarwme"></a>
|
183 |
+
# SoarWME:
|
184 |
+
A class which can represent a WMElement with an `(<id> ^att value)` but takes care of actually interfacing with working memory
|
185 |
+
|
186 |
+
You can update its value whenever you want, it will not affect working memory. To change working memory, call `add_to_wm`, `update_wm`, and `remove_from_wm` during an event callback (like BEFORE_INPUT_PHASE)
|
187 |
+
|
188 |
+
|
189 |
+
<a name="svscommands"></a>
|
190 |
+
# SVSCommands:
|
191 |
+
A collection of helper functions to create string commands that can be send to SVS
|
192 |
+
Here pos, rot, and scl are lists of 3 numbers (like [1, 2.5, 3.1])
|
193 |
+
|
194 |
+
* `add_box(obj_id, pos=None, rot=None, scl=None)`
|
195 |
+
* `change_pos(obj_id, pos)`
|
196 |
+
* `change_rot(obj_id, rot)`
|
197 |
+
* `change_scl(obj_id, scl)`
|
198 |
+
* `delete(obj_id)`
|
199 |
+
* `add_tag(obj_id, tag_name, tag_value)`
|
200 |
+
* `change_tag(obj_id, tag_name, tag_value)`
|
201 |
+
* `delete_tag(obj_id, tag_name)`
|
202 |
+
|
203 |
+
<a name="timeconnector"></a>
|
204 |
+
# TimeConnector
|
205 |
+
An AgentConnector that will create time info on the input-link.
|
206 |
+
Includes elapsed time since the agent started, and can have a real-time or simulated wall clock.
|
207 |
+
It is enabled through the client setting `use-time-connector=True`
|
208 |
+
There are several settings that control its behavior as described in [Config Settings](#timesettings).
|
209 |
+
|
210 |
+
|
211 |
+
```
|
212 |
+
# Will add and update the following on the input-link:
|
213 |
+
([il] ^time [t])
|
214 |
+
([t] ^seconds [secs] # real-time seconds elapsed since start of agent
|
215 |
+
^milliseconds [ms] # real-time milliseconds elapsed since start
|
216 |
+
^steps [steps] # number of decision cycles since start of agent
|
217 |
+
^clock [clock])
|
218 |
+
([clock] ^hour [hr] # 0-23
|
219 |
+
^minute [min] # 0-59
|
220 |
+
^second [sec] # 0-59
|
221 |
+
^millisecond [ms] # 0-999
|
222 |
+
^epoch [sec] # Unix epoch time in seconds)
|
223 |
+
```
|
224 |
+
|
225 |
+
Also, if using a simulated clock, the agent can change the time itself using an output command:
|
226 |
+
```
|
227 |
+
([out] ^set-time [cmd])
|
228 |
+
([cmd] ^hour 9
|
229 |
+
^minute 15
|
230 |
+
^second 30) # optional
|
231 |
+
```
|
232 |
+
|
233 |
+
<a name="util"></a>
|
234 |
+
# pysoarlib.util
|
235 |
+
Package containing several utility functions for reading/writing working memory through sml structures.
|
236 |
+
|
237 |
+
#### `parse_wm_printout(text:str)`
|
238 |
+
|
239 |
+
Given a printout of soar's working memory (p S1 -d 4), parses it into a dictionary of wmes,
|
240 |
+
where the keys are identifiers, and the values are lists of wme triples rooted at that id.
|
241 |
+
|
242 |
+
You can wrap the result with a PrintoutIdentifier(wmes, root_id) which will provide an Identifier-like
|
243 |
+
iterface for crawling over the graph structure. It provides all the methods in the IdentifierExtensions interface.
|
244 |
+
|
245 |
+
|
246 |
+
#### `extract_wm_graph(root_id, max_depth)`
|
247 |
+
|
248 |
+
Recursively explores all working memory reachable from the given root_id (up to max_depth),
|
249 |
+
builds up a graph structure representing all that information.
|
250 |
+
|
251 |
+
Note: max_depth is optional (defaults to no depth limit), and the function is smart about handling cycles (will not recurse forever)
|
252 |
+
|
253 |
+
```
|
254 |
+
# Returns a WMNode object wrapping the root_id and containing links to children
|
255 |
+
node.id = root_id (Identifier)
|
256 |
+
node.symbol = string (The root_id symbol e.g. O34)
|
257 |
+
node.attributes() - returns a list of child attribute strings
|
258 |
+
node['attr'] = WMNode # for child identifiers
|
259 |
+
node['attr'] = constant # for string, double, or int value
|
260 |
+
node['attr'] = [ val1, val2, ... ] # for multi-valued attributes
|
261 |
+
(values can be constants or WMNodes)
|
262 |
+
str(node) - will pretty-print the node and all children recursively
|
263 |
+
```
|
264 |
+
|
265 |
+
|
266 |
+
#### `update_wm_from_tree(root_id, root_name, input_dict, wme_table)`
|
267 |
+
|
268 |
+
Will update working memory using the given `input_dict` as the provided structure rooted at `root_id`.
|
269 |
+
Created wme's are stored in the given `wme_table`, which should be a dictionary that is kept across
|
270 |
+
multiple calls to this function. `root_name` specifies a prefix for each wme name in the wme_table.
|
271 |
+
|
272 |
+
```
|
273 |
+
# input_dict should have the following structure:
|
274 |
+
{
|
275 |
+
'attr1': getter() <- The value will be the result of calling the given getter function
|
276 |
+
'attr2': dict <- The value will be a child identifier with its own recursive substructure
|
277 |
+
}
|
278 |
+
```
|
279 |
+
|
280 |
+
#### `remove_tree_from_wm(wme_table)`
|
281 |
+
|
282 |
+
Given a wme_table filled by `SoarUtils.update_wm_from_tree`, removes all wmes from working memory
|
283 |
+
|
284 |
+
|
285 |
+
|
pysoarlib/SVSCommands.py
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module defines a set of methods that generate SVS string commands
|
3 |
+
"""
|
4 |
+
class SVSCommands:
|
5 |
+
""" Contains static methods that generate SVS string commands
|
6 |
+
|
7 |
+
These can then be passed to agent.SendSVSCommands
|
8 |
+
Note that all transforms (pos, rot, scale) should be lists of 3 floats
|
9 |
+
"""
|
10 |
+
@staticmethod
|
11 |
+
def pos_to_str(pos):
|
12 |
+
""" Returns a string of 3 space-separated position values """
|
13 |
+
return "{:f} {:f} {:f}".format(pos[0], pos[1], pos[2])
|
14 |
+
|
15 |
+
@staticmethod
|
16 |
+
def rot_to_str(rot):
|
17 |
+
""" Returns a string of 3 space-separated rotation values """
|
18 |
+
return "{:f} {:f} {:f}".format(rot[0], rot[1], rot[2])
|
19 |
+
|
20 |
+
@staticmethod
|
21 |
+
def scl_to_str(scl):
|
22 |
+
""" Returns a string of 3 space-separated scale values """
|
23 |
+
return "{:f} {:f} {:f}".format(scl[0], scl[1], scl[2])
|
24 |
+
|
25 |
+
@staticmethod
|
26 |
+
def bbox_verts():
|
27 |
+
""" Returns a string of 8 vertices (24 numbers) forming a bounding box
|
28 |
+
|
29 |
+
It is of unit size centered at the origin
|
30 |
+
"""
|
31 |
+
return "0.5 0.5 0.5 0.5 0.5 -0.5 0.5 -0.5 0.5 0.5 -0.5 -0.5 -0.5 0.5 0.5 -0.5 0.5 -0.5 -0.5 -0.5 0.5 -0.5 -0.5 -0.5"
|
32 |
+
|
33 |
+
@staticmethod
|
34 |
+
def add_node(node_id, pos=None, rot=None, scl=None, parent="world"):
|
35 |
+
""" Returns an SVS command for adding a graph node to the scene (no geometry) """
|
36 |
+
cmd = "add {:s} {:s} ".format(node_id, parent)
|
37 |
+
if pos: cmd += " p {:s}".format(SVSCommands.pos_to_str(pos))
|
38 |
+
if rot: cmd += " r {:s}".format(SVSCommands.rot_to_str(rot))
|
39 |
+
if scl: cmd += " s {:s}".format(SVSCommands.scl_to_str(scl))
|
40 |
+
return cmd
|
41 |
+
|
42 |
+
@staticmethod
|
43 |
+
def add_box(obj_id, pos=None, rot=None, scl=None, parent="world"):
|
44 |
+
""" Returns an SVS command for adding a bounding box object to the scene """
|
45 |
+
cmd = "add {:s} {:s} v {:s}".format(obj_id, parent, SVSCommands.bbox_verts())
|
46 |
+
if pos: cmd += " p {:s}".format(SVSCommands.pos_to_str(pos))
|
47 |
+
if rot: cmd += " r {:s}".format(SVSCommands.rot_to_str(rot))
|
48 |
+
if scl: cmd += " s {:s}".format(SVSCommands.scl_to_str(scl))
|
49 |
+
return cmd
|
50 |
+
|
51 |
+
@staticmethod
|
52 |
+
def change_pos(obj_id, pos):
|
53 |
+
""" Returns an SVS command for changing the position of an svs object """
|
54 |
+
return "change {:s} p {:s}".format(obj_id, SVSCommands.pos_to_str(pos))
|
55 |
+
|
56 |
+
@staticmethod
|
57 |
+
def change_rot(obj_id, rot):
|
58 |
+
""" Returns an SVS command for changing the rotation of an svs object """
|
59 |
+
return "change {:s} r {:s}".format(obj_id, SVSCommands.rot_to_str(rot))
|
60 |
+
|
61 |
+
@staticmethod
|
62 |
+
def change_scl(obj_id, scl):
|
63 |
+
""" Returns an SVS command for changing the scale of an svs object """
|
64 |
+
return "change {:s} s {:s}".format(obj_id, SVSCommands.scl_to_str(scl))
|
65 |
+
|
66 |
+
@staticmethod
|
67 |
+
def delete(obj_id):
|
68 |
+
""" Returns an SVS command for deleting an object """
|
69 |
+
return "delete {:s}".format(obj_id)
|
70 |
+
|
71 |
+
@staticmethod
|
72 |
+
def add_tag(obj_id, tag_name, tag_value):
|
73 |
+
""" Returns an SVS command for adding a tag to an object (^name value) """
|
74 |
+
return "tag add {:s} {:s} {:s}".format(obj_id, tag_name, tag_value)
|
75 |
+
|
76 |
+
@staticmethod
|
77 |
+
def change_tag(obj_id, tag_name, tag_value):
|
78 |
+
""" Returns an SVS command for changing a tag on an object (^name value) """
|
79 |
+
return "tag change {:s} {:s} {:s}".format(obj_id, tag_name, tag_value)
|
80 |
+
|
81 |
+
@staticmethod
|
82 |
+
def delete_tag(obj_id, tag_name):
|
83 |
+
""" Returns an SVS command for deleting a tag with the given name from an object """
|
84 |
+
return "tag delete {:s} {:s}".format(obj_id, tag_name)
|
pysoarlib/SoarClient.py
ADDED
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from __future__ import print_function
|
2 |
+
|
3 |
+
from threading import Thread
|
4 |
+
import traceback
|
5 |
+
from time import sleep
|
6 |
+
|
7 |
+
import Python_sml_ClientInterface as sml
|
8 |
+
from .SoarWME import SoarWME
|
9 |
+
from .TimeConnector import TimeConnector
|
10 |
+
|
11 |
+
class SoarClient():
|
12 |
+
""" A wrapper class for creating and using a soar SML Agent """
|
13 |
+
def __init__(self, print_handler=None, config_filename=None, **kwargs):
|
14 |
+
""" Will create a soar kernel and agent
|
15 |
+
|
16 |
+
print_handler determines how output is printed, defaults to python print
|
17 |
+
config_filename if specified will read config info (kwargs) from a file
|
18 |
+
Config file is a text file with lines of the form 'setting = value'
|
19 |
+
|
20 |
+
============== kwargs =============
|
21 |
+
|
22 |
+
agent_name = [string] (default=soaragent)
|
23 |
+
Name to give the SML Agent when it is created
|
24 |
+
|
25 |
+
agent_source = [filename] (default=None)
|
26 |
+
Soar file to source when the agent is created
|
27 |
+
|
28 |
+
smem_source = [filename] (default=None)
|
29 |
+
Soar file with smem add commands to source the agent is created
|
30 |
+
|
31 |
+
source_output = full|summary|none (default=summary)
|
32 |
+
Determines how much output is printed when sourcing files
|
33 |
+
|
34 |
+
watch_level = [int] (default=1)
|
35 |
+
The watch level to use (controls amount of info printed, 0=none, 5=all)
|
36 |
+
|
37 |
+
start_running = true|false (default=false)
|
38 |
+
If true, will immediately start the agent running
|
39 |
+
|
40 |
+
spawn_debugger = true|false (default=false)
|
41 |
+
If true, will spawn the java soar debugger
|
42 |
+
|
43 |
+
write_to_stdout = true|false (default=false)
|
44 |
+
If true, will print all soar output to the given print_handler (default is python print)
|
45 |
+
|
46 |
+
enable_log = true|false
|
47 |
+
If true, will write all soar output to a file given by log_filename
|
48 |
+
|
49 |
+
log_filename = [filename] (default = agent-log.txt)
|
50 |
+
Specify the name of the log file to write
|
51 |
+
|
52 |
+
remote_connection = true|false (default=false)
|
53 |
+
If true, will connect to a remote kernel instead of creating a new one
|
54 |
+
|
55 |
+
use_time_connector = true|false (default=false)
|
56 |
+
If true, will create a TimeConnector to add time info the the input-link
|
57 |
+
See the Readme or TimeConnector.py for additional settings to control its behavior
|
58 |
+
|
59 |
+
Note: Still need to call connect() to register event handlers
|
60 |
+
"""
|
61 |
+
|
62 |
+
self.print_handler = print_handler
|
63 |
+
if print_handler == None:
|
64 |
+
self.print_handler = print
|
65 |
+
self.print_event_handlers = []
|
66 |
+
|
67 |
+
self.connectors = {}
|
68 |
+
|
69 |
+
# Gather settings, filling in defaults as needed
|
70 |
+
self.kwarg_keys = set(kwargs.keys())
|
71 |
+
self.settings = kwargs
|
72 |
+
self.config_filename = config_filename
|
73 |
+
self._read_config_file()
|
74 |
+
self._apply_settings()
|
75 |
+
|
76 |
+
self.connected = False
|
77 |
+
self.is_running = False
|
78 |
+
self.queue_stop = False
|
79 |
+
|
80 |
+
self.run_event_callback_id = -1
|
81 |
+
self.print_event_callback_id = -1
|
82 |
+
self.init_agent_callback_id = -1
|
83 |
+
|
84 |
+
if self.remote_connection:
|
85 |
+
self.kernel = sml.Kernel.CreateRemoteConnection()
|
86 |
+
else:
|
87 |
+
self.kernel = sml.Kernel.CreateKernelInNewThread()
|
88 |
+
self.kernel.SetAutoCommit(False)
|
89 |
+
|
90 |
+
if self.use_time_connector:
|
91 |
+
self.add_connector("time", TimeConnector(self, **self.settings))
|
92 |
+
self._create_soar_agent()
|
93 |
+
|
94 |
+
def add_connector(self, name, connector):
|
95 |
+
""" Adds an AgentConnector to the agent """
|
96 |
+
self.connectors[name] = connector
|
97 |
+
|
98 |
+
def has_connector(self, name):
|
99 |
+
""" Returns True if the agent has an AgentConnector with the given name """
|
100 |
+
return (name in self.connectors)
|
101 |
+
|
102 |
+
def get_connector(self, name):
|
103 |
+
""" Returns the AgentConnector with the given name, or None """
|
104 |
+
return self.connectors.get(name, None)
|
105 |
+
|
106 |
+
def add_print_event_handler(self, handler):
|
107 |
+
""" calls the given handler during each soar print event,
|
108 |
+
where handler is a method taking a single string argument """
|
109 |
+
self.print_event_handlers.append(handler)
|
110 |
+
|
111 |
+
def start(self):
|
112 |
+
""" Will start the agent (uses another thread, so non-blocking) """
|
113 |
+
if self.is_running:
|
114 |
+
return
|
115 |
+
|
116 |
+
self.is_running = True
|
117 |
+
thread = Thread(target = SoarClient._run_thread, args = (self, ))
|
118 |
+
thread.start()
|
119 |
+
|
120 |
+
def stop(self):
|
121 |
+
""" Tell the running thread to stop
|
122 |
+
|
123 |
+
Note: Non-blocking, agent may run for a bit after this call finishes"""
|
124 |
+
self.queue_stop = True
|
125 |
+
|
126 |
+
def execute_command(self, cmd, print_res=False):
|
127 |
+
""" Execute a soar command and return result,
|
128 |
+
write output to print_handler if print_res is True """
|
129 |
+
result = self.agent.ExecuteCommandLine(cmd).strip()
|
130 |
+
if print_res:
|
131 |
+
self.print_handler(cmd)
|
132 |
+
self.print_handler(result)
|
133 |
+
return result
|
134 |
+
|
135 |
+
def connect(self):
|
136 |
+
""" Register event handlers for agent and connectors """
|
137 |
+
if self.connected:
|
138 |
+
return
|
139 |
+
|
140 |
+
self.run_event_callback_id = self.agent.RegisterForRunEvent(
|
141 |
+
sml.smlEVENT_BEFORE_INPUT_PHASE, SoarClient._run_event_handler, self)
|
142 |
+
|
143 |
+
self.print_event_callback_id = self.agent.RegisterForPrintEvent(
|
144 |
+
sml.smlEVENT_PRINT, SoarClient._print_event_handler, self)
|
145 |
+
|
146 |
+
self.init_agent_callback_id = self.kernel.RegisterForAgentEvent(
|
147 |
+
sml.smlEVENT_BEFORE_AGENT_REINITIALIZED, SoarClient._init_agent_handler, self)
|
148 |
+
|
149 |
+
for connector in self.connectors.values():
|
150 |
+
connector.connect()
|
151 |
+
|
152 |
+
self.connected = True
|
153 |
+
|
154 |
+
if self.start_running:
|
155 |
+
self.start()
|
156 |
+
|
157 |
+
def disconnect(self):
|
158 |
+
""" Unregister event handlers for agent and connectors """
|
159 |
+
if not self.connected:
|
160 |
+
return
|
161 |
+
|
162 |
+
if self.run_event_callback_id != -1:
|
163 |
+
self.agent.UnregisterForRunEvent(self.run_event_callback_id)
|
164 |
+
self.run_event_callback_id = -1
|
165 |
+
|
166 |
+
if self.print_event_callback_id != -1:
|
167 |
+
self.agent.UnregisterForPrintEvent(self.print_event_callback_id)
|
168 |
+
self.print_event_callback_id = -1
|
169 |
+
|
170 |
+
if self.init_agent_callback_id != -1:
|
171 |
+
self.kernel.UnregisterForAgentEvent(self.init_agent_callback_id)
|
172 |
+
self.init_agent_callback_id = -1
|
173 |
+
|
174 |
+
for connector in self.connectors.values():
|
175 |
+
connector.disconnect()
|
176 |
+
|
177 |
+
self.connected = False
|
178 |
+
|
179 |
+
def reset(self):
|
180 |
+
""" Will destroy the current agent and create + source a new one """
|
181 |
+
self._destroy_soar_agent()
|
182 |
+
self._create_soar_agent()
|
183 |
+
self.connect()
|
184 |
+
|
185 |
+
def kill(self):
|
186 |
+
""" Will destroy the current agent + kernel, cleans up everything """
|
187 |
+
self._destroy_soar_agent()
|
188 |
+
self.kernel.Shutdown()
|
189 |
+
self.kernel = None
|
190 |
+
|
191 |
+
#### Internal Methods
|
192 |
+
def _read_config_file(self):
|
193 |
+
""" Will read the given config file and update self.settings as necessary (wont overwrite kwarg settings)
|
194 |
+
|
195 |
+
config_filename is a text file with lines of the form 'setting = value'"""
|
196 |
+
|
197 |
+
if self.config_filename is None:
|
198 |
+
return
|
199 |
+
|
200 |
+
# Add any settings in the config file (if it exists)
|
201 |
+
try:
|
202 |
+
with open(self.config_filename, 'r') as fin:
|
203 |
+
config_args = [ line.split() for line in fin ]
|
204 |
+
|
205 |
+
for args in config_args:
|
206 |
+
if len(args) == 3 and args[1] == '=':
|
207 |
+
key = args[0].replace("-", "_")
|
208 |
+
# Add settings from config file if not overridden in kwargs
|
209 |
+
if key not in self.kwarg_keys:
|
210 |
+
self.settings[key] = args[2]
|
211 |
+
except IOError:
|
212 |
+
pass
|
213 |
+
|
214 |
+
def _apply_settings(self):
|
215 |
+
""" Set up the SoarClient object by copying settings or filling in default values """
|
216 |
+
self.agent_name = self.settings.get("agent_name", "soaragent")
|
217 |
+
self.agent_source = self.settings.get("agent_source", None)
|
218 |
+
self.smem_source = self.settings.get("smem_source", None)
|
219 |
+
|
220 |
+
self.source_output = self.settings.get("source_output", "summary")
|
221 |
+
self.watch_level = int(self.settings.get("watch_level", 1))
|
222 |
+
self.remote_connection = self._parse_bool_setting("remote_connection", False)
|
223 |
+
self.spawn_debugger = self._parse_bool_setting("spawn_debugger", False)
|
224 |
+
self.start_running = self._parse_bool_setting("start_running", False)
|
225 |
+
self.write_to_stdout = self._parse_bool_setting("write_to_stdout", False)
|
226 |
+
self.enable_log = self._parse_bool_setting("enable_log", False)
|
227 |
+
self.log_filename = self.settings.get("log_filename", "agent-log.txt")
|
228 |
+
self.use_time_connector = self._parse_bool_setting("use_time_connector", False)
|
229 |
+
|
230 |
+
def _parse_bool_setting(self, name, default):
|
231 |
+
if name not in self.settings:
|
232 |
+
return default
|
233 |
+
val = self.settings[name]
|
234 |
+
if type(val) == str:
|
235 |
+
return val.lower() == "true"
|
236 |
+
return val
|
237 |
+
|
238 |
+
def _run_thread(self):
|
239 |
+
self.agent.ExecuteCommandLine("run")
|
240 |
+
self.is_running = False
|
241 |
+
|
242 |
+
def _create_soar_agent(self):
|
243 |
+
self.log_writer = None
|
244 |
+
if self.enable_log:
|
245 |
+
try:
|
246 |
+
self.log_writer = open(self.log_filename, 'w')
|
247 |
+
except:
|
248 |
+
self.print_handler("ERROR: Cannot open log file " + self.log_filename)
|
249 |
+
|
250 |
+
if self.remote_connection:
|
251 |
+
self.agent = self.kernel.GetAgentByIndex(0)
|
252 |
+
else:
|
253 |
+
self.agent = self.kernel.CreateAgent(self.agent_name)
|
254 |
+
self._source_agent()
|
255 |
+
|
256 |
+
if self.spawn_debugger:
|
257 |
+
success = self.agent.SpawnDebugger(self.kernel.GetListenerPort())
|
258 |
+
|
259 |
+
self.agent.ExecuteCommandLine("w " + str(self.watch_level))
|
260 |
+
|
261 |
+
def _source_agent(self):
|
262 |
+
self.agent.ExecuteCommandLine("smem --set database memory")
|
263 |
+
self.agent.ExecuteCommandLine("epmem --set database memory")
|
264 |
+
|
265 |
+
if self.smem_source != None:
|
266 |
+
if self.source_output != "none":
|
267 |
+
self.print_handler("------------- SOURCING SMEM ---------------")
|
268 |
+
result = self.agent.ExecuteCommandLine("source " + self.smem_source)
|
269 |
+
if self.source_output == "full":
|
270 |
+
self.print_handler(result)
|
271 |
+
elif self.source_output == "summary":
|
272 |
+
self._summarize_smem_source(result)
|
273 |
+
|
274 |
+
if self.agent_source != None:
|
275 |
+
if self.source_output != "none":
|
276 |
+
self.print_handler("--------- SOURCING PRODUCTIONS ------------")
|
277 |
+
result = self.agent.ExecuteCommandLine("source " + self.agent_source + " -v")
|
278 |
+
if self.source_output == "full":
|
279 |
+
self.print_handler(result)
|
280 |
+
elif self.source_output == "summary":
|
281 |
+
self._summarize_source(result)
|
282 |
+
else:
|
283 |
+
self.print_handler("agent_source not specified, no rules are being sourced")
|
284 |
+
|
285 |
+
# Prints a summary of the smem source command instead of every line (source_output = summary)
|
286 |
+
def _summarize_smem_source(self, printout):
|
287 |
+
summary = []
|
288 |
+
n_added = 0
|
289 |
+
for line in printout.split('\n'):
|
290 |
+
if line == "Knowledge added to semantic memory.":
|
291 |
+
n_added += 1
|
292 |
+
else:
|
293 |
+
summary.append(line)
|
294 |
+
self.print_handler('\n'.join(summary))
|
295 |
+
self.print_handler("Knowledge added to semantic memory. [" + str(n_added) + " times]")
|
296 |
+
|
297 |
+
# Prints a summary of the agent source command instead of every line (source_output = summary)
|
298 |
+
def _summarize_source(self, printout):
|
299 |
+
summary = []
|
300 |
+
for line in printout.split('\n'):
|
301 |
+
if line.startswith("Sourcing"):
|
302 |
+
continue
|
303 |
+
if line.startswith("warnings is now"):
|
304 |
+
continue
|
305 |
+
# Line is only * or # characters
|
306 |
+
if all(c in "#* " for c in line):
|
307 |
+
continue
|
308 |
+
summary.append(line)
|
309 |
+
self.print_handler('\n'.join(summary))
|
310 |
+
|
311 |
+
def _on_init_soar(self):
|
312 |
+
for connector in self.connectors.values():
|
313 |
+
connector.on_init_soar()
|
314 |
+
|
315 |
+
def _destroy_soar_agent(self):
|
316 |
+
self.stop()
|
317 |
+
while self.is_running:
|
318 |
+
sleep(0.01)
|
319 |
+
self._on_init_soar()
|
320 |
+
self.disconnect()
|
321 |
+
if self.spawn_debugger:
|
322 |
+
self.agent.KillDebugger()
|
323 |
+
if not self.remote_connection:
|
324 |
+
self.kernel.DestroyAgent(self.agent)
|
325 |
+
self.agent = None
|
326 |
+
if self.log_writer is not None:
|
327 |
+
self.log_writer.close()
|
328 |
+
self.log_writer = None
|
329 |
+
|
330 |
+
@staticmethod
|
331 |
+
def _init_agent_handler(eventID, self, info):
|
332 |
+
try:
|
333 |
+
self._on_init_soar()
|
334 |
+
except:
|
335 |
+
self.print_handler("ERROR IN INIT AGENT")
|
336 |
+
self.print_handler(traceback.format_exc())
|
337 |
+
|
338 |
+
@staticmethod
|
339 |
+
def _run_event_handler(eventID, self, agent, phase):
|
340 |
+
if eventID == sml.smlEVENT_BEFORE_INPUT_PHASE:
|
341 |
+
self._on_input_phase(agent.GetInputLink())
|
342 |
+
|
343 |
+
def _on_input_phase(self, input_link):
|
344 |
+
try:
|
345 |
+
if self.queue_stop:
|
346 |
+
self.agent.StopSelf()
|
347 |
+
self.queue_stop = False
|
348 |
+
|
349 |
+
|
350 |
+
for connector in self.connectors.values():
|
351 |
+
connector.on_input_phase(input_link)
|
352 |
+
|
353 |
+
if self.agent.IsCommitRequired():
|
354 |
+
self.agent.Commit()
|
355 |
+
except:
|
356 |
+
self.print_handler("ERROR IN RUN HANDLER")
|
357 |
+
self.print_handler(traceback.format_exc())
|
358 |
+
|
359 |
+
|
360 |
+
@staticmethod
|
361 |
+
def _print_event_handler(eventID, self, agent, message):
|
362 |
+
try:
|
363 |
+
if self.write_to_stdout:
|
364 |
+
message = message.strip()
|
365 |
+
self.print_handler(message)
|
366 |
+
if self.log_writer:
|
367 |
+
self.log_writer.write(message)
|
368 |
+
self.log_writer.flush()
|
369 |
+
for ph in self.print_event_handlers:
|
370 |
+
ph(message)
|
371 |
+
except:
|
372 |
+
self.print_handler("ERROR IN PRINT HANDLER")
|
373 |
+
self.print_handler(traceback.format_exc())
|
374 |
+
|
375 |
+
|
pysoarlib/SoarWME.py
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module defines a utility class called SoarWME
|
3 |
+
which wraps SML code for adding/removing Soar Working Memory Elements (WME)
|
4 |
+
"""
|
5 |
+
|
6 |
+
from .WMInterface import WMInterface
|
7 |
+
|
8 |
+
class SoarWME(WMInterface):
|
9 |
+
""" Wrapper for a single Soar Working Memory Element with a primitive value
|
10 |
+
|
11 |
+
It can wrap an int, float, or string type
|
12 |
+
|
13 |
+
An instance is not directly tied to an SML wme,
|
14 |
+
the user decides how and when soar's working memory is modified
|
15 |
+
|
16 |
+
So you can change the value anytime (asynchronously to soar)
|
17 |
+
And then modify working memory via add_to_wm, update_wm, and remove_from_wm
|
18 |
+
during an agent callback (like BEFORE_INPUT_PHASE)
|
19 |
+
"""
|
20 |
+
|
21 |
+
def __init__(self, att, val):
|
22 |
+
""" Initializes the wme, but does not add to working memory yet
|
23 |
+
|
24 |
+
:param att: The wme's attribute
|
25 |
+
:type att: str
|
26 |
+
|
27 |
+
:param val: The wme's value, any of the 3 main primitive types
|
28 |
+
:type val: int, float, or str
|
29 |
+
"""
|
30 |
+
WMInterface.__init__(self)
|
31 |
+
self.att = att
|
32 |
+
self.val = val
|
33 |
+
self.wme = None
|
34 |
+
|
35 |
+
self.changed = False
|
36 |
+
|
37 |
+
if type(val) == int:
|
38 |
+
self.create_wme = self._create_int_wme
|
39 |
+
elif type(val) == float:
|
40 |
+
self.create_wme = self._create_float_wme
|
41 |
+
else:
|
42 |
+
self.create_wme = self._create_string_wme
|
43 |
+
|
44 |
+
def get_attr(self):
|
45 |
+
""" Returns the wme's attribute """
|
46 |
+
return self.att
|
47 |
+
|
48 |
+
def get_value(self):
|
49 |
+
""" Returns the wme's value """
|
50 |
+
return self.val
|
51 |
+
|
52 |
+
def set_value(self, newval):
|
53 |
+
""" Set's the wme's value, but also need to call update_wm to change working memory """
|
54 |
+
if self.val != newval:
|
55 |
+
self.val = newval
|
56 |
+
self.changed = True
|
57 |
+
|
58 |
+
def __str__(self):
|
59 |
+
return str(self.val)
|
60 |
+
|
61 |
+
|
62 |
+
### Internal Methods
|
63 |
+
|
64 |
+
def _create_int_wme(self, id, att, val):
|
65 |
+
return id.CreateIntWME(att, val)
|
66 |
+
|
67 |
+
def _create_float_wme(self, id, att, val):
|
68 |
+
return id.CreateFloatWME(att, val)
|
69 |
+
|
70 |
+
def _create_string_wme(self, id, att, val):
|
71 |
+
return id.CreateStringWME(att, str(val))
|
72 |
+
|
73 |
+
def _add_to_wm_impl(self, parent_id):
|
74 |
+
""" Creates a wme in soar's working memory rooted at the given parent_id """
|
75 |
+
self.wme = self.create_wme(parent_id, self.att, self.val)
|
76 |
+
|
77 |
+
def _update_wm_impl(self):
|
78 |
+
""" If the value has changed, will update soar's working memory with the new value """
|
79 |
+
if self.changed:
|
80 |
+
self.wme.Update(self.val)
|
81 |
+
self.changed = False
|
82 |
+
|
83 |
+
def _remove_from_wm_impl(self):
|
84 |
+
""" Will remove the wme from soar's working memory """
|
85 |
+
self.wme.DestroyWME()
|
86 |
+
self.wme = None
|
87 |
+
|
pysoarlib/TimeConnector.py
ADDED
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import time
|
3 |
+
import datetime
|
4 |
+
current_time_ms = lambda: int(round(time.time() * 1000))
|
5 |
+
|
6 |
+
from .AgentConnector import AgentConnector
|
7 |
+
from .SoarWME import SoarWME
|
8 |
+
|
9 |
+
class TimeConnector(AgentConnector):
|
10 |
+
""" An agent connector that will maintain time info on the input-link
|
11 |
+
|
12 |
+
The input link will look like:
|
13 |
+
(<il> ^time <t>)
|
14 |
+
(<t> ^seconds <secs> # real-time seconds elapsed since start of agent
|
15 |
+
^milliseconds <ms> # real-time milliseconds elapsed since start
|
16 |
+
^steps <steps> # number of decision cycles since start of agent
|
17 |
+
^clock <clock>)
|
18 |
+
(<clock> ^hour <hr> # 0-23
|
19 |
+
^minute <min> # 0-59
|
20 |
+
^second <sec> # 0-59
|
21 |
+
^millisecond <ms> # 0-999
|
22 |
+
^epoch <sec> # Unix epoch time in seconds)
|
23 |
+
|
24 |
+
Also, if using a simulated clock, the agent can send the following output-command:
|
25 |
+
(<out> ^set-time <st>) (<st> ^hour <h> ^minute <min> ^second <sec>)
|
26 |
+
|
27 |
+
Settings:
|
28 |
+
clock_include_ms: bool [default=True]
|
29 |
+
If true, includes milliseconds with both elapsed time and clock time
|
30 |
+
sim_clock: bool [default=False]
|
31 |
+
If true, uses a simulated clock that starts at 8AM and advances a fixed amount every DC
|
32 |
+
If false, will use the local real time
|
33 |
+
clock_step_ms: int [default=5000]
|
34 |
+
If using the simulated clock, this is the number of milliseconds it will increase every DC
|
35 |
+
|
36 |
+
"""
|
37 |
+
def __init__(self, client, clock_include_ms=True, sim_clock=False, clock_step_ms=50, **kwargs):
|
38 |
+
""" Initializes the connector with the time info
|
39 |
+
|
40 |
+
clock_include_ms - If True: will include millisecond resolution on clock/elapsed
|
41 |
+
(Setting to false will mean fewer changes to the input-link, slightly faster)
|
42 |
+
sim_clock - If False: clock uses real-time. If True: clock is simulated
|
43 |
+
clock_step_ms - If sim_clock=True, this is how much the clock advances every DC
|
44 |
+
"""
|
45 |
+
AgentConnector.__init__(self, client)
|
46 |
+
|
47 |
+
self.include_ms = clock_include_ms
|
48 |
+
self.sim_clock = sim_clock
|
49 |
+
self.clock_step_ms = int(clock_step_ms)
|
50 |
+
|
51 |
+
self.time_id = None
|
52 |
+
self.seconds = SoarWME("seconds", 0) # number of real-time seconds elapsed since start of agent
|
53 |
+
self.milsecs = SoarWME("milliseconds", 0) # number of real-time milliseconds elapsed since start of agent
|
54 |
+
self.steps = SoarWME("steps", 0) # number of decision cycles the agent has taken
|
55 |
+
|
56 |
+
# Output Link Command: (<out> ^set-time <st>) (<st> ^hour <h> ^minute <min> ^second <sec>)
|
57 |
+
self.add_output_command("set-time")
|
58 |
+
|
59 |
+
# Clock info, hour minute second millisecond
|
60 |
+
self.clock_id = None
|
61 |
+
self.clock_info = [0, 0, 0, 0, 0]
|
62 |
+
self.clock_wmes = [ SoarWME("hour", 0), SoarWME("minute", 0), SoarWME("second", 0), SoarWME("millisecond", 0), SoarWME("epoch", 0) ]
|
63 |
+
self.reset_time()
|
64 |
+
|
65 |
+
def advance_clock(self, num_ms):
|
66 |
+
""" Advances the simulated clock by the given number of milliseconds """
|
67 |
+
self.clock_info[3] += num_ms
|
68 |
+
# MS
|
69 |
+
if self.clock_info[3] >= 1000:
|
70 |
+
self.clock_info[2] += self.clock_info[3] // 1000
|
71 |
+
self.clock_info[4] += self.clock_info[3] // 1000
|
72 |
+
self.clock_info[3] = self.clock_info[3] % 1000
|
73 |
+
# Seconds
|
74 |
+
if self.clock_info[2] >= 60:
|
75 |
+
self.clock_info[1] += self.clock_info[2] // 60
|
76 |
+
self.clock_info[2] = self.clock_info[2] % 60
|
77 |
+
# Minutes
|
78 |
+
if self.clock_info[1] >= 60:
|
79 |
+
self.clock_info[0] += self.clock_info[1] // 60
|
80 |
+
self.clock_info[1] = self.clock_info[1] % 60
|
81 |
+
# Hours
|
82 |
+
self.clock_info[0] = self.clock_info[0] % 24
|
83 |
+
|
84 |
+
def update_clock(self):
|
85 |
+
""" Updates the clock with the real time """
|
86 |
+
localtime = time.localtime()
|
87 |
+
self.clock_info[0] = localtime.tm_hour
|
88 |
+
self.clock_info[1] = localtime.tm_min
|
89 |
+
self.clock_info[2] = localtime.tm_sec
|
90 |
+
self.clock_info[3] = current_time_ms() % 1000
|
91 |
+
self.clock_info[4] = int(time.time())
|
92 |
+
|
93 |
+
def reset_time(self):
|
94 |
+
""" Resets the time info """
|
95 |
+
# If simulating clock, default epoch is Jan 1, 2020 at 8 AM
|
96 |
+
default_epoch = int(time.mktime(datetime.datetime(2020, 1, 1, 8, 0, 0, 0).timetuple()))
|
97 |
+
self.clock_info = [8, 0, 0, 0, default_epoch] # [ hour, min, sec, ms, epoch ]
|
98 |
+
self.milsecs.set_value(0)
|
99 |
+
self.seconds.set_value(0)
|
100 |
+
self.steps.set_value(0)
|
101 |
+
self.start_time = current_time_ms()
|
102 |
+
|
103 |
+
def on_init_soar(self):
|
104 |
+
self._remove_from_wm()
|
105 |
+
self.reset_time()
|
106 |
+
|
107 |
+
def set_time(self, hour, min, sec=0, ms=0):
|
108 |
+
if not self.sim_clock:
|
109 |
+
return
|
110 |
+
self.clock_info[0] = hour
|
111 |
+
self.clock_info[1] = (0 if min is None else min)
|
112 |
+
self.clock_info[2] = (0 if sec is None else sec)
|
113 |
+
self.clock_info[3] = ms
|
114 |
+
self.clock_info[4] = int(time.mktime(datetime.datetime(2020, 1, 1, hour, min, sec, ms).timetuple()))
|
115 |
+
|
116 |
+
def on_input_phase(self, input_link):
|
117 |
+
# Update the global timers (time since agent start)
|
118 |
+
self.milsecs.set_value(int(current_time_ms() - self.start_time))
|
119 |
+
self.seconds.set_value(int((current_time_ms() - self.start_time)/1000))
|
120 |
+
self.steps.set_value(self.steps.get_value() + 1)
|
121 |
+
|
122 |
+
# Update the clock, either real-time or simulated
|
123 |
+
if self.sim_clock:
|
124 |
+
self.advance_clock(self.clock_step_ms)
|
125 |
+
else:
|
126 |
+
self.update_clock()
|
127 |
+
|
128 |
+
# Update working memory
|
129 |
+
if self.time_id is None:
|
130 |
+
self._add_to_wm(input_link)
|
131 |
+
else:
|
132 |
+
self._update_wm()
|
133 |
+
|
134 |
+
def on_output_event(self, command_name, root_id):
|
135 |
+
if command_name == "set-time":
|
136 |
+
self.process_set_time_command(root_id)
|
137 |
+
|
138 |
+
def process_set_time_command(self, time_id):
|
139 |
+
h = time_id.GetChildInt('hour')
|
140 |
+
m = time_id.GetChildInt('minute')
|
141 |
+
s = time_id.GetChildInt('second')
|
142 |
+
self.set_time(h, m, s)
|
143 |
+
time_id.CreateStringWME('status', 'complete')
|
144 |
+
|
145 |
+
### Internal methods
|
146 |
+
|
147 |
+
def _add_to_wm(self, parent_id):
|
148 |
+
self.time_id = parent_id.CreateIdWME("time")
|
149 |
+
if self.include_ms:
|
150 |
+
self.milsecs.add_to_wm(self.time_id)
|
151 |
+
self.seconds.add_to_wm(self.time_id)
|
152 |
+
self.steps.add_to_wm(self.time_id)
|
153 |
+
|
154 |
+
self.clock_id = self.time_id.CreateIdWME("clock")
|
155 |
+
for i, wme in enumerate(self.clock_wmes):
|
156 |
+
if i == 3 and not self.include_ms:
|
157 |
+
continue
|
158 |
+
wme.set_value(self.clock_info[i])
|
159 |
+
wme.add_to_wm(self.clock_id)
|
160 |
+
|
161 |
+
def _update_wm(self):
|
162 |
+
if self.include_ms:
|
163 |
+
self.milsecs.update_wm()
|
164 |
+
self.seconds.update_wm()
|
165 |
+
self.steps.update_wm()
|
166 |
+
for i, wme in enumerate(self.clock_wmes):
|
167 |
+
wme.set_value(self.clock_info[i])
|
168 |
+
wme.update_wm()
|
169 |
+
|
170 |
+
def _remove_from_wm(self):
|
171 |
+
if self.time_id is None:
|
172 |
+
return
|
173 |
+
for wme in self.clock_wmes:
|
174 |
+
wme.remove_from_wm()
|
175 |
+
self.milsecs.remove_from_wm()
|
176 |
+
self.seconds.remove_from_wm()
|
177 |
+
self.steps.remove_from_wm()
|
178 |
+
self.time_id.DestroyWME()
|
179 |
+
self.time_id = None
|
180 |
+
self.clock_id = None
|
181 |
+
|
pysoarlib/WMInterface.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
This module defines a utility interface called WMInterface
|
3 |
+
which defines a standard interface for adding and removing things from working memory
|
4 |
+
"""
|
5 |
+
|
6 |
+
class WMInterface(object):
|
7 |
+
""" An interface standardizing how to add/remove items from working memory """
|
8 |
+
|
9 |
+
def __init__(self):
|
10 |
+
self.added = False
|
11 |
+
|
12 |
+
def is_added(self):
|
13 |
+
""" Returns true if the wme is currently in soar's working memory """
|
14 |
+
return self.added
|
15 |
+
|
16 |
+
def add_to_wm(self, parent_id):
|
17 |
+
""" Creates a structure in working memory rooted at the given parent_id """
|
18 |
+
if self.added:
|
19 |
+
self._remove_from_wm_impl()
|
20 |
+
self._add_to_wm_impl(parent_id)
|
21 |
+
self.added = True
|
22 |
+
|
23 |
+
def update_wm(self, parent_id = None):
|
24 |
+
""" Updates the structure in Soar's working memory
|
25 |
+
It will also add it to wm if parent_id is not None """
|
26 |
+
if self.added:
|
27 |
+
self._update_wm_impl()
|
28 |
+
elif parent_id:
|
29 |
+
self._add_to_wm_impl(parent_id)
|
30 |
+
self.added = True
|
31 |
+
|
32 |
+
def remove_from_wm(self):
|
33 |
+
""" Removes the structure from Soar's working memory """
|
34 |
+
if not self.added:
|
35 |
+
return
|
36 |
+
self._remove_from_wm_impl()
|
37 |
+
self.added = False
|
38 |
+
|
39 |
+
|
40 |
+
### Internal Methods - To be implemented by derived classes
|
41 |
+
|
42 |
+
def _add_to_wm_impl(self, parent_id):
|
43 |
+
""" Method to implement in derived class - add to working memory """
|
44 |
+
pass
|
45 |
+
|
46 |
+
def _update_wm_impl(self):
|
47 |
+
""" Method to implement in derived class - update working memory """
|
48 |
+
pass
|
49 |
+
|
50 |
+
def _remove_from_wm_impl(self):
|
51 |
+
""" Method to implement in derived class - remove from working memory """
|
52 |
+
pass
|
53 |
+
|
pysoarlib/__init__.py
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
""" Helper classes and functions for creating a soar agent and working with SML
|
2 |
+
|
3 |
+
Depends on the Python_sml_ClientInterface, so make sure that SOAR_HOME is on the PYTHONPATH
|
4 |
+
|
5 |
+
SoarClient and AgentConnector are used to create an agent
|
6 |
+
WMInterface is a standardized interface for adding/removing structures from working memory
|
7 |
+
SoarWME is a wrapper for creating working memory elements
|
8 |
+
SVSCommands will generate svs command strings for some common use cases
|
9 |
+
|
10 |
+
Also adds helper methods to the Identifier class to access children more easily
|
11 |
+
(See IdentifierExtensions)
|
12 |
+
|
13 |
+
"""
|
14 |
+
import Python_sml_ClientInterface as sml
|
15 |
+
|
16 |
+
__all__ = ["WMInterface", "SoarWME", "SVSCommands", "AgentConnector", "SoarClient", "TimeConnector"]
|
17 |
+
|
18 |
+
# Extend the sml Identifier class definition with additional utility methods
|
19 |
+
from .IdentifierExtensions import *
|
20 |
+
sml.Identifier.GetChildString = get_child_str
|
21 |
+
sml.Identifier.GetChildInt = get_child_int
|
22 |
+
sml.Identifier.GetChildFloat = get_child_float
|
23 |
+
sml.Identifier.GetChildId = get_child_id
|
24 |
+
sml.Identifier.GetAllChildIds = get_all_child_ids
|
25 |
+
sml.Identifier.GetAllChildValues = get_all_child_values
|
26 |
+
sml.Identifier.GetAllChildWmes = get_all_child_wmes
|
27 |
+
sml.Identifier.__lt__ = lambda self, other: self.GetIdentifierSymbol() < other.GetIdentifierSymbol()
|
28 |
+
|
29 |
+
from .WMInterface import WMInterface
|
30 |
+
from .SoarWME import SoarWME
|
31 |
+
from .SVSCommands import SVSCommands
|
32 |
+
from .AgentConnector import AgentConnector
|
33 |
+
from .SoarClient import SoarClient
|
34 |
+
from .TimeConnector import TimeConnector
|
35 |
+
|
36 |
+
|
pysoarlib/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (1.54 kB). View file
|
|
pysoarlib/util/PrintoutIdentifier.py
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pysoarlib.util import parse_wm_printout
|
2 |
+
|
3 |
+
class PrintoutIdentifier:
|
4 |
+
""" Represents an identifier that was parsed from a soar print command via parse_wm_printout
|
5 |
+
and implements the IdentifierExtensions interface for it """
|
6 |
+
|
7 |
+
def create(client, id, depth):
|
8 |
+
""" Will print the given identifier to the given depth and wrap the result in a PrintoutIdentifier """
|
9 |
+
printout = client.execute_command("p " + id + " -d " + str(depth))
|
10 |
+
if printout.strip().startswith("There is no identifier"):
|
11 |
+
return None
|
12 |
+
wmes = parse_wm_printout(printout)
|
13 |
+
return PrintoutIdentifier(wmes, id)
|
14 |
+
|
15 |
+
def __init__(self, wmes, root_id):
|
16 |
+
""" wmes is the result of a parse_wm_printout command,
|
17 |
+
root_id is the str id for this identifier """
|
18 |
+
self.wmes = wmes
|
19 |
+
self.root_id = root_id
|
20 |
+
|
21 |
+
def __lt__(self, other):
|
22 |
+
return self.root_id < other.root_id
|
23 |
+
|
24 |
+
def GetIdentifierSymbol(self):
|
25 |
+
return self.root_id
|
26 |
+
|
27 |
+
def GetChildString(self, attr):
|
28 |
+
return self._get_value(attr)
|
29 |
+
|
30 |
+
def GetChildInt(self, attr):
|
31 |
+
val = self._get_value(attr)
|
32 |
+
try:
|
33 |
+
return int(val)
|
34 |
+
except ValueError:
|
35 |
+
return None
|
36 |
+
|
37 |
+
def GetChildFloat(self, attr):
|
38 |
+
val = self._get_value(attr)
|
39 |
+
try:
|
40 |
+
return float(val)
|
41 |
+
except ValueError:
|
42 |
+
return None
|
43 |
+
|
44 |
+
def GetChildId(self, attr):
|
45 |
+
child_id = self._get_value(attr)
|
46 |
+
if child_id is not None:
|
47 |
+
return PrintoutIdentifier(self.wmes, child_id)
|
48 |
+
return None
|
49 |
+
|
50 |
+
def GetAllChildIds(self, attr=None):
|
51 |
+
# Get all children whose values are also identifiers in the wmes dict
|
52 |
+
child_wmes = [ wme for wme in self.wmes.get(self.root_id, []) if wme[2] in self.wmes ]
|
53 |
+
if attr is not None:
|
54 |
+
child_wmes = [ wme for wme in child_wmes if wme[1] == attr ]
|
55 |
+
return [ PrintoutIdentifier(self.wmes, wme[2]) for wme in child_wmes ]
|
56 |
+
|
57 |
+
def GetAllChildValues(self, attr=None):
|
58 |
+
# Get all children whose values are not identifiers in the wmes dict
|
59 |
+
child_wmes = [ wme for wme in self.wmes.get(self.root_id, []) if wme[2] not in self.wmes ]
|
60 |
+
if attr is not None:
|
61 |
+
child_wmes = [ wme for wme in child_wmes if wme[1] == attr ]
|
62 |
+
return [ wme[2] for wme in child_wmes ]
|
63 |
+
|
64 |
+
def GetAllChildWmes(self):
|
65 |
+
child_wmes = []
|
66 |
+
for wme in self.wmes.get(self.root_id, []):
|
67 |
+
if wme[2] in self.wmes:
|
68 |
+
# Identifier
|
69 |
+
child_wmes.append( (wme[1], PrintoutIdentifier(self.wmes, wme[2])) )
|
70 |
+
else:
|
71 |
+
# Value
|
72 |
+
child_wmes.append( (wme[1], wme[2]) )
|
73 |
+
return child_wmes
|
74 |
+
|
75 |
+
def _get_value(self, attr):
|
76 |
+
return next((wme[2] for wme in self.wmes.get(self.root_id, []) if wme[1] == attr), None)
|
pysoarlib/util/WMNode.py
ADDED
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Python_sml_ClientInterface as sml
|
2 |
+
|
3 |
+
### Note: Helper class used by extract_wm_graph
|
4 |
+
|
5 |
+
class WMNode:
|
6 |
+
""" Represents a node in the working memory graph wrapping an Identifier and containing links to child wmes
|
7 |
+
|
8 |
+
node.id = root_id (Identifier)
|
9 |
+
node.symbol = string (The root_id symbol e.g. O34)
|
10 |
+
node['attr'] = WMNode # for identifiers
|
11 |
+
node['attr'] = constant # for string, double, or int value
|
12 |
+
node['attr'] = [ val1, val2, ... ] # for multi-valued attributes (values can be constants or WMNodes)
|
13 |
+
"""
|
14 |
+
|
15 |
+
def __init__(self, soar_id):
|
16 |
+
self.id = soar_id
|
17 |
+
self.symbol = soar_id.GetValueAsString()
|
18 |
+
self.children = {}
|
19 |
+
|
20 |
+
def attributes(self):
|
21 |
+
""" Returns a list of all child wme attribute strings """
|
22 |
+
return list(self.children.keys())
|
23 |
+
|
24 |
+
# Supports dictionary syntax (read only)
|
25 |
+
def __getitem__(self, attr):
|
26 |
+
""" Returns the value of the wme (node, attr, val)
|
27 |
+
where a value can be a int, double, string, WMNode,
|
28 |
+
or a list of such values for a multi-valued attribute """
|
29 |
+
return self.children.get(attr, None)
|
30 |
+
|
31 |
+
def __str__(self):
|
32 |
+
""" Returns a nicely formatted string representation of the node and all its children
|
33 |
+
(Warning: will be a lot of text for large graphs) """
|
34 |
+
return self.__str_helper__("", set())
|
35 |
+
|
36 |
+
def __str_helper__(self, indent, ignore_ids):
|
37 |
+
var = "<" + self.symbol + ">"
|
38 |
+
if self.symbol in ignore_ids or len(self.children) == 0:
|
39 |
+
return var
|
40 |
+
|
41 |
+
ignore_ids.add(self.symbol)
|
42 |
+
|
43 |
+
s = var + " {\n"
|
44 |
+
for a, v in self.children.items():
|
45 |
+
s += indent + " " + a + ": " + _wm_value_to_str(v, indent + " ", ignore_ids) + "\n"
|
46 |
+
s += indent + "}"
|
47 |
+
return s
|
48 |
+
|
49 |
+
def _extract_children(self, max_depth, node_map):
|
50 |
+
""" Internal helper method to recursively extract graph structure for a node's children """
|
51 |
+
if max_depth == 0:
|
52 |
+
return
|
53 |
+
|
54 |
+
for index in range(self.id.GetNumberChildren()):
|
55 |
+
wme = self.id.GetChild(index)
|
56 |
+
attr = wme.GetAttribute()
|
57 |
+
if wme.IsIdentifier():
|
58 |
+
child_id = wme.ConvertToIdentifier()
|
59 |
+
child_sym = child_id.GetValueAsString()
|
60 |
+
# First check if the child id is already in the node map
|
61 |
+
if child_sym in node_map:
|
62 |
+
wme_val = node_map[child_sym]
|
63 |
+
else:
|
64 |
+
# If not, recursively create and extract the children
|
65 |
+
wme_val = WMNode(child_id)
|
66 |
+
node_map[wme_val.symbol] = wme_val
|
67 |
+
wme_val._extract_children(max_depth-1, node_map)
|
68 |
+
|
69 |
+
elif wme.GetValueType() == "int":
|
70 |
+
wme_val = wme.ConvertToIntElement().GetValue()
|
71 |
+
elif wme.GetValueType() == "double":
|
72 |
+
wme_val = wme.ConvertToFloatElement().GetValue()
|
73 |
+
else:
|
74 |
+
wme_val = wme.GetValueAsString()
|
75 |
+
|
76 |
+
self._add_child_wme(attr, wme_val)
|
77 |
+
|
78 |
+
def _add_child_wme(self, attr, value):
|
79 |
+
""" Adds the child wme to the children dictionary
|
80 |
+
If there are multiple values for a given attr, move them into a list instead of replacing """
|
81 |
+
if attr in self.children:
|
82 |
+
cur_val = self.children[attr]
|
83 |
+
if isinstance(cur_val, list):
|
84 |
+
# Child is already a list, just append
|
85 |
+
cur_val.append(value)
|
86 |
+
else:
|
87 |
+
# This is the second value for the attr, replace current value with a list
|
88 |
+
self.children[attr] = [ cur_val, value ]
|
89 |
+
else:
|
90 |
+
# First time we've seen this attr, just add to dictionary
|
91 |
+
self.children[attr] = value
|
92 |
+
|
93 |
+
def _wm_value_to_str(val, indent, ignore_ids):
|
94 |
+
"""
|
95 |
+
recursive helper function which returns a string representation of any given value type
|
96 |
+
|
97 |
+
:param val: The value to convert to a string (can be str, int, float, list, WMNode)
|
98 |
+
:param indent: a string of spaces to indent the current level
|
99 |
+
:param ignore_ids: A set of Identifier symbols to not print
|
100 |
+
"""
|
101 |
+
if isinstance(val, str):
|
102 |
+
return val
|
103 |
+
if isinstance(val, int):
|
104 |
+
return str(val)
|
105 |
+
if isinstance(val, float):
|
106 |
+
return str(val)
|
107 |
+
if isinstance(val, list):
|
108 |
+
return "[ " + ", ".join(_wm_value_to_str(i, indent, ignore_ids) for i in val) + " ]"
|
109 |
+
if isinstance(val, WMNode):
|
110 |
+
return val.__str_helper__(indent, ignore_ids)
|
111 |
+
return ""
|
112 |
+
|
113 |
+
|
pysoarlib/util/__init__.py
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
__all__ = ["extract_wm_graph", "parse_wm_printout", "PrintoutIdentifier", "update_wm_from_tree", "remove_tree_from_wm" ]
|
3 |
+
|
4 |
+
from .extract_wm_graph import extract_wm_graph
|
5 |
+
from .parse_wm_printout import parse_wm_printout
|
6 |
+
from .update_wm_from_tree import update_wm_from_tree
|
7 |
+
from .remove_tree_from_wm import remove_tree_from_wm
|
8 |
+
from .PrintoutIdentifier import PrintoutIdentifier
|
pysoarlib/util/extract_wm_graph.py
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from .WMNode import WMNode
|
2 |
+
|
3 |
+
def extract_wm_graph(root_id, max_depth=-1):
|
4 |
+
""" Given a soar identifier (root_id), crawls over the children and builds a graph rep for them
|
5 |
+
This will handle cycles, where the same node will be used for each reference to an identifier
|
6 |
+
|
7 |
+
:param root_id: The sml identifier of the root of the sub-graph
|
8 |
+
:param max_depth: The maximum depth to extract (defaults to unlimited depth)
|
9 |
+
:return a WMNode containing a recursive enumeration of all children reachable from the given root_id
|
10 |
+
|
11 |
+
Example:
|
12 |
+
|
13 |
+
Given an identifier <obj> with the following wm structure:
|
14 |
+
(<obj> ^id 5 ^volume 23.3 ^predicates <preds>)
|
15 |
+
(<preds> ^predicate red ^predicate cube ^predicate block)
|
16 |
+
|
17 |
+
Will return the following WMNode:
|
18 |
+
WMNode root_node
|
19 |
+
.id = sml Identifier for <obj>
|
20 |
+
.symbol = 'O32'
|
21 |
+
['id'] = 5
|
22 |
+
['volume'] = 23.3
|
23 |
+
['predicates'] = WMNode
|
24 |
+
.id = sml Identifier for <preds>
|
25 |
+
.symbol = 'P53'
|
26 |
+
['predicate'] = [ 'red', 'cube', 'block' ]
|
27 |
+
"""
|
28 |
+
root_node = WMNode(root_id)
|
29 |
+
node_map = dict()
|
30 |
+
node_map[root_node.symbol] = root_node
|
31 |
+
root_node._extract_children(max_depth, node_map)
|
32 |
+
return root_node
|
33 |
+
|
pysoarlib/util/parse_wm_printout.py
ADDED
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
def parse_wm_printout(text):
|
3 |
+
""" Given a printout of soar's working memory, parses it into a dictionary of wmes,
|
4 |
+
Where the keys are identifiers, and the values are lists of wme triples rooted at that id
|
5 |
+
|
6 |
+
:param text: The output of a soar print command for working memory
|
7 |
+
:type text: str
|
8 |
+
|
9 |
+
:returns dict{ str, list[ (str, str, str) ] }
|
10 |
+
|
11 |
+
"""
|
12 |
+
|
13 |
+
### First: preprocess the output into a string of tokens
|
14 |
+
tokens = []
|
15 |
+
quote = None
|
16 |
+
for word in text.split():
|
17 |
+
# Handle quoted strings (Between | |)
|
18 |
+
if word[0] == '|':
|
19 |
+
quote = word
|
20 |
+
elif quote is not None:
|
21 |
+
quote += ' ' + word
|
22 |
+
if quote is not None:
|
23 |
+
if len(quote) > 1 and quote.endswith('|'):
|
24 |
+
tokens.append(quote)
|
25 |
+
quote = None
|
26 |
+
elif len(quote) > 1 and quote.endswith('|)'):
|
27 |
+
tokens.append(quote[:-1])
|
28 |
+
quote = None
|
29 |
+
continue
|
30 |
+
|
31 |
+
# Ignore operator preferences
|
32 |
+
if word in [ '+', '>', '<', '!', '=' ]:
|
33 |
+
continue
|
34 |
+
# Ignore activation values [+23.000]
|
35 |
+
if word.startswith("[+") and (word.endswith("]") or word.endswith("])")):
|
36 |
+
continue
|
37 |
+
# Ignore singleton lti's (@12533)
|
38 |
+
if word.startswith("(@") and word.endswith(")"):
|
39 |
+
continue
|
40 |
+
# Strip opening parens but add $ to indicate identifier
|
41 |
+
if word.startswith("("):
|
42 |
+
word = '$' + word[1:]
|
43 |
+
|
44 |
+
# Don't care about closing parens
|
45 |
+
word = word.replace(")", "")
|
46 |
+
tokens.append(word)
|
47 |
+
|
48 |
+
wmes = dict()
|
49 |
+
cur_id = None
|
50 |
+
cur_att = None
|
51 |
+
cur_wmes = []
|
52 |
+
i = 0
|
53 |
+
|
54 |
+
for token in tokens:
|
55 |
+
if len(token) == 0:
|
56 |
+
continue
|
57 |
+
# Identifier
|
58 |
+
if token[0] == '$':
|
59 |
+
cur_id = token[1:]
|
60 |
+
cur_att = None
|
61 |
+
cur_wmes = []
|
62 |
+
wmes[cur_id] = cur_wmes
|
63 |
+
# Attribute
|
64 |
+
elif token[0] == '^':
|
65 |
+
cur_att = token[1:]
|
66 |
+
# Value
|
67 |
+
elif cur_id is None:
|
68 |
+
print("ERROR: Value " + token + " encountered with no id")
|
69 |
+
elif cur_att is None:
|
70 |
+
print("ERROR: Value " + token + " encountered with no attribute")
|
71 |
+
else:
|
72 |
+
cur_wmes.append( (cur_id, cur_att, token) )
|
73 |
+
|
74 |
+
return wmes
|
75 |
+
|
pysoarlib/util/remove_tree_from_wm.py
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
def remove_tree_from_wm(wme_table):
|
3 |
+
"""
|
4 |
+
Given a wme_table filled by SoarUtils.update_wm_from_tree, removes all wmes from working memory
|
5 |
+
|
6 |
+
Intermediate nodes are sml.Identifiers, which are removed from the table
|
7 |
+
Leaves are SoarWME's which are kept in the table but .remove_from_wm() is called on them
|
8 |
+
"""
|
9 |
+
items_to_remove = set()
|
10 |
+
for path, wme in wme_table.items():
|
11 |
+
if isinstance(wme, sml.Identifier):
|
12 |
+
items_to_remove.add(path)
|
13 |
+
else:
|
14 |
+
wme.remove_from_wm()
|
15 |
+
for path in items_to_remove:
|
16 |
+
del wme_table[path]
|
17 |
+
|
18 |
+
|
pysoarlib/util/update_wm_from_tree.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pysoarlib import SoarWME
|
2 |
+
|
3 |
+
def update_wm_from_tree(root_id, root_name, input_dict, wme_table):
|
4 |
+
"""
|
5 |
+
Recursively update WMEs that have a sub-tree structure rooted at the given identifier.
|
6 |
+
|
7 |
+
We scan through the `input_dict`, which represents the input value getters (or further
|
8 |
+
sub-trees) of the sub-tree root, either adding terminal WMEs as usual or further recursing.
|
9 |
+
|
10 |
+
:param root_id: The sml identifier of the root of the sub-tree
|
11 |
+
:param root_name: The attribute which is the root of this sub-tree
|
12 |
+
:param input_dict: A dict mapping attributes to getter functions
|
13 |
+
:param wme_table: A table to lookup and store wme's and identifiers
|
14 |
+
:return: None
|
15 |
+
"""
|
16 |
+
assert isinstance(input_dict, dict), "Should only recurse on dicts!"
|
17 |
+
|
18 |
+
for attribute in input_dict.keys():
|
19 |
+
input_val = input_dict[attribute]
|
20 |
+
child_name = root_name + "." + attribute
|
21 |
+
|
22 |
+
if not callable(input_val):
|
23 |
+
if child_name not in wme_table:
|
24 |
+
wme_table[child_name] = root_id.CreateIdWME(attribute)
|
25 |
+
child_id = wme_table[child_name]
|
26 |
+
SoarUtils.update_wm_from_tree(child_id, child_name, input_val, wme_table)
|
27 |
+
continue
|
28 |
+
|
29 |
+
value = input_val()
|
30 |
+
if child_name not in wme_table:
|
31 |
+
wme_table[child_name] = SoarWME(att=attribute, val=value)
|
32 |
+
wme = wme_table[child_name]
|
33 |
+
wme.set_value(value)
|
34 |
+
wme.update_wm(root_id)
|
35 |
+
|
requirements.txt
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
gradio
|
2 |
+
gcc7
|
soar_io.py
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pysoarlib import *
|
2 |
+
|
3 |
+
## DEFINE MAIN CONNECTOR CLASS
|
4 |
+
|
5 |
+
class GeneralSoarIOConnector(AgentConnector):
|
6 |
+
def __init__(self, client):
|
7 |
+
AgentConnector.__init__(self, client)
|
8 |
+
client.add_print_event_handler(self.agent_print_collector)
|
9 |
+
# client.execute_command("output callbacks on")
|
10 |
+
# client.execute_command("output console on")
|
11 |
+
|
12 |
+
self.agent_printout = ""
|
13 |
+
|
14 |
+
def on_input_phase(self, input_link):
|
15 |
+
pass
|
16 |
+
|
17 |
+
def on_init_soar(self):
|
18 |
+
self.reset()
|
19 |
+
|
20 |
+
def on_output_event(self, command_name, root_id):
|
21 |
+
pass
|
22 |
+
|
23 |
+
|
24 |
+
def agent_print_collector(self, text):
|
25 |
+
self.agent_printout += text+"\n"
|
26 |
+
|
27 |
+
def get_agent_output(self):
|
28 |
+
return self.agent_printout
|
29 |
+
|
30 |
+
def reset(self):
|
31 |
+
self.agent_printout = ""
|
32 |
+
|
33 |
+
|
34 |
+
|
35 |
+
|