bryan-stearns commited on
Commit
2130399
1 Parent(s): e83fdc6

Adding py files

Browse files
.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: gray
5
- colorTo: pink
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
+