|
from __future__ import print_function |
|
|
|
from threading import Thread |
|
import traceback |
|
from time import sleep |
|
|
|
import Python_sml_ClientInterface as sml |
|
from .SoarWME import SoarWME |
|
from .TimeConnector import TimeConnector |
|
|
|
class SoarClient(): |
|
""" A wrapper class for creating and using a soar SML Agent """ |
|
def __init__(self, print_handler=None, config_filename=None, **kwargs): |
|
""" Will create a soar kernel and agent |
|
|
|
print_handler determines how output is printed, defaults to python print |
|
config_filename if specified will read config info (kwargs) from a file |
|
Config file is a text file with lines of the form 'setting = value' |
|
|
|
============== kwargs ============= |
|
|
|
agent_name = [string] (default=soaragent) |
|
Name to give the SML Agent when it is created |
|
|
|
agent_source = [filename] (default=None) |
|
Soar file to source when the agent is created |
|
|
|
smem_source = [filename] (default=None) |
|
Soar file with smem add commands to source the agent is created |
|
|
|
source_output = full|summary|none (default=summary) |
|
Determines how much output is printed when sourcing files |
|
|
|
watch_level = [int] (default=1) |
|
The watch level to use (controls amount of info printed, 0=none, 5=all) |
|
|
|
start_running = true|false (default=false) |
|
If true, will immediately start the agent running |
|
|
|
spawn_debugger = true|false (default=false) |
|
If true, will spawn the java soar debugger |
|
|
|
write_to_stdout = true|false (default=false) |
|
If true, will print all soar output to the given print_handler (default is python print) |
|
|
|
enable_log = true|false |
|
If true, will write all soar output to a file given by log_filename |
|
|
|
log_filename = [filename] (default = agent-log.txt) |
|
Specify the name of the log file to write |
|
|
|
remote_connection = true|false (default=false) |
|
If true, will connect to a remote kernel instead of creating a new one |
|
|
|
use_time_connector = true|false (default=false) |
|
If true, will create a TimeConnector to add time info the the input-link |
|
See the Readme or TimeConnector.py for additional settings to control its behavior |
|
|
|
Note: Still need to call connect() to register event handlers |
|
""" |
|
|
|
self.print_handler = print_handler |
|
if print_handler == None: |
|
self.print_handler = print |
|
self.print_event_handlers = [] |
|
|
|
self.connectors = {} |
|
|
|
|
|
self.kwarg_keys = set(kwargs.keys()) |
|
self.settings = kwargs |
|
self.config_filename = config_filename |
|
self._read_config_file() |
|
self._apply_settings() |
|
|
|
self.connected = False |
|
self.is_running = False |
|
self.queue_stop = False |
|
|
|
self.run_event_callback_id = -1 |
|
self.print_event_callback_id = -1 |
|
self.init_agent_callback_id = -1 |
|
|
|
if self.remote_connection: |
|
self.kernel = sml.Kernel.CreateRemoteConnection() |
|
else: |
|
self.kernel = sml.Kernel.CreateKernelInNewThread() |
|
self.kernel.SetAutoCommit(False) |
|
|
|
if self.use_time_connector: |
|
self.add_connector("time", TimeConnector(self, **self.settings)) |
|
self._create_soar_agent() |
|
|
|
def add_connector(self, name, connector): |
|
""" Adds an AgentConnector to the agent """ |
|
self.connectors[name] = connector |
|
|
|
def has_connector(self, name): |
|
""" Returns True if the agent has an AgentConnector with the given name """ |
|
return (name in self.connectors) |
|
|
|
def get_connector(self, name): |
|
""" Returns the AgentConnector with the given name, or None """ |
|
return self.connectors.get(name, None) |
|
|
|
def add_print_event_handler(self, handler): |
|
""" calls the given handler during each soar print event, |
|
where handler is a method taking a single string argument """ |
|
self.print_event_handlers.append(handler) |
|
|
|
def start(self): |
|
""" Will start the agent (uses another thread, so non-blocking) """ |
|
if self.is_running: |
|
return |
|
|
|
self.is_running = True |
|
thread = Thread(target = SoarClient._run_thread, args = (self, )) |
|
thread.start() |
|
|
|
def stop(self): |
|
""" Tell the running thread to stop |
|
|
|
Note: Non-blocking, agent may run for a bit after this call finishes""" |
|
self.queue_stop = True |
|
|
|
def execute_command(self, cmd, print_res=False): |
|
""" Execute a soar command and return result, |
|
write output to print_handler if print_res is True """ |
|
result = self.agent.ExecuteCommandLine(cmd).strip() |
|
if print_res: |
|
self.print_handler(cmd) |
|
self.print_handler(result) |
|
return result |
|
|
|
def connect(self): |
|
""" Register event handlers for agent and connectors """ |
|
if self.connected: |
|
return |
|
|
|
self.run_event_callback_id = self.agent.RegisterForRunEvent( |
|
sml.smlEVENT_BEFORE_INPUT_PHASE, SoarClient._run_event_handler, self) |
|
|
|
self.print_event_callback_id = self.agent.RegisterForPrintEvent( |
|
sml.smlEVENT_PRINT, SoarClient._print_event_handler, self) |
|
|
|
self.init_agent_callback_id = self.kernel.RegisterForAgentEvent( |
|
sml.smlEVENT_BEFORE_AGENT_REINITIALIZED, SoarClient._init_agent_handler, self) |
|
|
|
for connector in self.connectors.values(): |
|
connector.connect() |
|
|
|
self.connected = True |
|
|
|
if self.start_running: |
|
self.start() |
|
|
|
def disconnect(self): |
|
""" Unregister event handlers for agent and connectors """ |
|
if not self.connected: |
|
return |
|
|
|
if self.run_event_callback_id != -1: |
|
self.agent.UnregisterForRunEvent(self.run_event_callback_id) |
|
self.run_event_callback_id = -1 |
|
|
|
if self.print_event_callback_id != -1: |
|
self.agent.UnregisterForPrintEvent(self.print_event_callback_id) |
|
self.print_event_callback_id = -1 |
|
|
|
if self.init_agent_callback_id != -1: |
|
self.kernel.UnregisterForAgentEvent(self.init_agent_callback_id) |
|
self.init_agent_callback_id = -1 |
|
|
|
for connector in self.connectors.values(): |
|
connector.disconnect() |
|
|
|
self.connected = False |
|
|
|
def reset(self): |
|
""" Will destroy the current agent and create + source a new one """ |
|
self._destroy_soar_agent() |
|
self._create_soar_agent() |
|
self.connect() |
|
|
|
def kill(self): |
|
""" Will destroy the current agent + kernel, cleans up everything """ |
|
self._destroy_soar_agent() |
|
self.kernel.Shutdown() |
|
self.kernel = None |
|
|
|
|
|
def _read_config_file(self): |
|
""" Will read the given config file and update self.settings as necessary (wont overwrite kwarg settings) |
|
|
|
config_filename is a text file with lines of the form 'setting = value'""" |
|
|
|
if self.config_filename is None: |
|
return |
|
|
|
|
|
try: |
|
with open(self.config_filename, 'r') as fin: |
|
config_args = [ line.split() for line in fin ] |
|
|
|
for args in config_args: |
|
if len(args) == 3 and args[1] == '=': |
|
key = args[0].replace("-", "_") |
|
|
|
if key not in self.kwarg_keys: |
|
self.settings[key] = args[2] |
|
except IOError: |
|
pass |
|
|
|
def _apply_settings(self): |
|
""" Set up the SoarClient object by copying settings or filling in default values """ |
|
self.agent_name = self.settings.get("agent_name", "soaragent") |
|
self.agent_source = self.settings.get("agent_source", None) |
|
self.smem_source = self.settings.get("smem_source", None) |
|
|
|
self.source_output = self.settings.get("source_output", "summary") |
|
self.watch_level = int(self.settings.get("watch_level", 1)) |
|
self.remote_connection = self._parse_bool_setting("remote_connection", False) |
|
self.spawn_debugger = self._parse_bool_setting("spawn_debugger", False) |
|
self.start_running = self._parse_bool_setting("start_running", False) |
|
self.write_to_stdout = self._parse_bool_setting("write_to_stdout", False) |
|
self.enable_log = self._parse_bool_setting("enable_log", False) |
|
self.log_filename = self.settings.get("log_filename", "agent-log.txt") |
|
self.use_time_connector = self._parse_bool_setting("use_time_connector", False) |
|
|
|
def _parse_bool_setting(self, name, default): |
|
if name not in self.settings: |
|
return default |
|
val = self.settings[name] |
|
if type(val) == str: |
|
return val.lower() == "true" |
|
return val |
|
|
|
def _run_thread(self): |
|
self.agent.ExecuteCommandLine("run") |
|
self.is_running = False |
|
|
|
def _create_soar_agent(self): |
|
self.log_writer = None |
|
if self.enable_log: |
|
try: |
|
self.log_writer = open(self.log_filename, 'w') |
|
except: |
|
self.print_handler("ERROR: Cannot open log file " + self.log_filename) |
|
|
|
if self.remote_connection: |
|
self.agent = self.kernel.GetAgentByIndex(0) |
|
else: |
|
self.agent = self.kernel.CreateAgent(self.agent_name) |
|
self._source_agent() |
|
|
|
if self.spawn_debugger: |
|
success = self.agent.SpawnDebugger(self.kernel.GetListenerPort()) |
|
|
|
self.agent.ExecuteCommandLine("w " + str(self.watch_level)) |
|
|
|
def _source_agent(self): |
|
self.agent.ExecuteCommandLine("smem --set database memory") |
|
self.agent.ExecuteCommandLine("epmem --set database memory") |
|
|
|
if self.smem_source != None: |
|
if self.source_output != "none": |
|
self.print_handler("------------- SOURCING SMEM ---------------") |
|
result = self.agent.ExecuteCommandLine("source " + self.smem_source) |
|
if self.source_output == "full": |
|
self.print_handler(result) |
|
elif self.source_output == "summary": |
|
self._summarize_smem_source(result) |
|
|
|
if self.agent_source != None: |
|
if self.source_output != "none": |
|
self.print_handler("--------- SOURCING PRODUCTIONS ------------") |
|
result = self.agent.ExecuteCommandLine("source " + self.agent_source + " -v") |
|
if self.source_output == "full": |
|
self.print_handler(result) |
|
elif self.source_output == "summary": |
|
self._summarize_source(result) |
|
else: |
|
self.print_handler("agent_source not specified, no rules are being sourced") |
|
|
|
|
|
def _summarize_smem_source(self, printout): |
|
summary = [] |
|
n_added = 0 |
|
for line in printout.split('\n'): |
|
if line == "Knowledge added to semantic memory.": |
|
n_added += 1 |
|
else: |
|
summary.append(line) |
|
self.print_handler('\n'.join(summary)) |
|
self.print_handler("Knowledge added to semantic memory. [" + str(n_added) + " times]") |
|
|
|
|
|
def _summarize_source(self, printout): |
|
summary = [] |
|
for line in printout.split('\n'): |
|
if line.startswith("Sourcing"): |
|
continue |
|
if line.startswith("warnings is now"): |
|
continue |
|
|
|
if all(c in "#* " for c in line): |
|
continue |
|
summary.append(line) |
|
self.print_handler('\n'.join(summary)) |
|
|
|
def _on_init_soar(self): |
|
for connector in self.connectors.values(): |
|
connector.on_init_soar() |
|
|
|
def _destroy_soar_agent(self): |
|
self.stop() |
|
while self.is_running: |
|
sleep(0.01) |
|
self._on_init_soar() |
|
self.disconnect() |
|
if self.spawn_debugger: |
|
self.agent.KillDebugger() |
|
if not self.remote_connection: |
|
self.kernel.DestroyAgent(self.agent) |
|
self.agent = None |
|
if self.log_writer is not None: |
|
self.log_writer.close() |
|
self.log_writer = None |
|
|
|
@staticmethod |
|
def _init_agent_handler(eventID, self, info): |
|
try: |
|
self._on_init_soar() |
|
except: |
|
self.print_handler("ERROR IN INIT AGENT") |
|
self.print_handler(traceback.format_exc()) |
|
|
|
@staticmethod |
|
def _run_event_handler(eventID, self, agent, phase): |
|
if eventID == sml.smlEVENT_BEFORE_INPUT_PHASE: |
|
self._on_input_phase(agent.GetInputLink()) |
|
|
|
def _on_input_phase(self, input_link): |
|
try: |
|
if self.queue_stop: |
|
self.agent.StopSelf() |
|
self.queue_stop = False |
|
|
|
|
|
for connector in self.connectors.values(): |
|
connector.on_input_phase(input_link) |
|
|
|
if self.agent.IsCommitRequired(): |
|
self.agent.Commit() |
|
except: |
|
self.print_handler("ERROR IN RUN HANDLER") |
|
self.print_handler(traceback.format_exc()) |
|
|
|
|
|
@staticmethod |
|
def _print_event_handler(eventID, self, agent, message): |
|
try: |
|
if self.write_to_stdout: |
|
message = message.strip() |
|
self.print_handler(message) |
|
if self.log_writer: |
|
self.log_writer.write(message) |
|
self.log_writer.flush() |
|
for ph in self.print_event_handlers: |
|
ph(message) |
|
except: |
|
self.print_handler("ERROR IN PRINT HANDLER") |
|
self.print_handler(traceback.format_exc()) |
|
|
|
|
|
|