def receive_eof(self): connection, _ = self.listen_socket.accept() print(connection) # socket_write = socket.SocketIO(connection, 'w') # self.out = io.BufferedWriter(socket_write) socket_read = socket.SocketIO(connection, 'r') inp_reader = KQMLReader(io.BufferedReader(socket_read)) # self.inp = inp_reader self.dispatcher = KQMLDispatcher(self, inp_reader, self.name) self.dispatcher.start()
def __init__(self, argv=None, **kwargs): defaults = dict(host='localhost', port=6200, is_application=False, testing=False, socket=None, name=None, group_name=None, scan_for_port=False, debug=False, do_register=True) self.dispatcher = None self.MAX_PORT_TRIES = 100 self.reply_id_counter = 1 if isinstance(argv, list): kwargs.update(translate_argv(argv)) elif argv is not None: raise KQMLException("Unusable type for keyord argument `argv`.") for kw, arg in kwargs.items(): if kw not in defaults.keys(): raise ValueError('Unexpected keyword argument: %s' % kw) else: defaults.pop(kw) self.__setattr__(kw, arg) for kw, arg in defaults.items(): self.__setattr__(kw, arg) if self.debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) if not self.testing: self.out = None self.inp = None logger.info('Using socket connection') conn = self.connect(self.host, self.port) if not conn: logger.error('Connection failed') self.exit(-1) assert self.inp is not None and self.out is not None, \ "Connection formed but input (%s) and output (%s) not set." % \ (self.inp, self.out) else: logger.info('Using stdio connection') self.out = io.BytesIO() self.inp = KQMLReader(io.BytesIO()) self.dispatcher = KQMLDispatcher(self, self.inp, self.name) if self.do_register: self.register()
def _listen(self): """Sets up input and output socket connections to our listener. Infinite loop while ready to connect to our listener socket. On connect we get the write socket as a Buffered Writer and the read socket as a KQML Reader (which passes through a Buffered Reader). The reader is then attached to a KQML Dispatcher which is subsequently started, and passed along to the executor (which is a thread pool manager). """ LOGGER.debug('listening') with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: while self.ready: connection, _ = self.listen_socket.accept() LOGGER.debug('received connection') socket_write = socket.SocketIO(connection, 'w') self.out = io.BufferedWriter(socket_write) socket_read = socket.SocketIO(connection, 'r') read_input = KQMLReader(io.BufferedReader(socket_read)) self.dispatcher = KQMLDispatcher(self, read_input, self.name) executor.submit(self.dispatcher.start)
def listen(self): logger.debug('listening') with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: while self.ready: conn, addr = self.listenSoc.accept() logger.debug('received connection') sw = socket.SocketIO(conn, 'w') self.out = io.BufferedWriter(sw) sr = socket.SocketIO(conn, 'r') inp = KQMLReader(io.BufferedReader(sr)) self.dispatcher = KQMLDispatcher(self, inp, self.name) executor.submit(self.dispatcher.start)
def listen(self): """Socket server, listens for new KQML messages and dispatches them accordingly. Doesn't necessarily need threading; Could just start dispatcher and after it returns accept next connection. This couldn't handle loads of inputs while being bogged down processing. To avoid this issue we thread the dispatching so the functions that get called are run in a separate Thread. We're using ThreadPoolExecutor because sockets use io, io is blocking and threads allow you to not block. """ with ThreadPoolExecutor(max_workers=5) as executor: while self.ready: connection, _ = self.listen_socket.accept() LOGGER.debug('Received connection: %s', connection) socket_write = SocketIO(connection, 'w') self.local_out = BufferedWriter(socket_write) socket_read = SocketIO(connection, 'r') read_input = KQMLReader(BufferedReader(socket_read)) self.dispatcher = KQMLDispatcher(self, read_input, self.name) LOGGER.debug('Starting dispatcher: %s', self.dispatcher) executor.submit(self.dispatcher.start) self.state = 'dispatching'
def __init__(self, argv=None, **kwargs): defaults = dict(host='localhost', port=6200, is_application=False, testing=False, socket=None, name=None, group_name=None, scan_for_port=False, debug=False) self.dispatcher = None self.MAX_PORT_TRIES = 100 self.reply_id_counter = 1 if isinstance(argv, list): kwargs.update(translate_argv(argv)) elif argv is not None: raise KQMLException("Unusable type for keyord argument `argv`.") for kw, arg in kwargs.items(): if kw not in defaults.keys(): raise ValueError('Unexpected keyword argument: %s' % kw) else: defaults.pop(kw) self.__setattr__(kw, arg) for kw, arg in defaults.items(): self.__setattr__(kw, arg) if self.debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) if not self.testing: self.out = None self.inp = None logger.info('Using socket connection') conn = self.connect(self.host, self.port) if not conn: logger.error('Connection failed') self.exit(-1) assert self.inp is not None and self.out is not None, \ "Connection formed but input (%s) and output (%s) not set." % \ (self.inp, self.out) else: logger.info('Using stdio connection') self.out = io.BytesIO() self.inp = KQMLReader(io.BytesIO()) self.dispatcher = KQMLDispatcher(self, self.inp, self.name) self.register()
class KQMLModule(object): def __init__(self, argv=None, **kwargs): defaults = dict(host='localhost', port=6200, is_application=False, testing=False, socket=None, name=None, group_name=None, scan_for_port=False, debug=False) self.dispatcher = None self.MAX_PORT_TRIES = 100 self.reply_id_counter = 1 if isinstance(argv, list): kwargs.update(translate_argv(argv)) elif argv is not None: raise KQMLException("Unusable type for keyord argument `argv`.") for kw, arg in kwargs.items(): if kw not in defaults.keys(): raise ValueError('Unexpected keyword argument: %s' % kw) else: defaults.pop(kw) self.__setattr__(kw, arg) for kw, arg in defaults.items(): self.__setattr__(kw, arg) if self.debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) if not self.testing: self.out = None self.inp = None logger.info('Using socket connection') conn = self.connect(self.host, self.port) if not conn: logger.error('Connection failed') self.exit(-1) assert self.inp is not None and self.out is not None, \ "Connection formed but input (%s) and output (%s) not set." % \ (self.inp, self.out) else: logger.info('Using stdio connection') self.out = io.BytesIO() self.inp = KQMLReader(io.BytesIO()) self.dispatcher = KQMLDispatcher(self, self.inp, self.name) self.register() def start(self): if not self.testing: self.dispatcher.start() def subscribe_request(self, req_type): msg = KQMLPerformative('subscribe') content = KQMLList('request') content.append('&key') content.set('content', KQMLList.from_string('(%s . *)' % req_type)) msg.set('content', content) self.send(msg) def subscribe_tell(self, tell_type): msg = KQMLPerformative('subscribe') content = KQMLList('tell') content.append('&key') content.set('content', KQMLList.from_string('(%s . *)' % tell_type)) msg.set('content', content) self.send(msg) def connect(self, host=None, startport=None): if host is None: host = self.host if startport is None: startport = self.port if not self.scan_for_port: return self.connect1(host, startport, True) else: maxtries = self.MAX_PORT_TRIES for port in range(startport, startport + maxtries): conn = self.connect1(host, port, False) if conn: return True logger.error('Failed to connect to ' + host + ':' + \ startport + '-' + port) return False def connect1(self, host, port, verbose=True): try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((host, port)) sw = socket.SocketIO(self.socket, 'w') self.out = io.BufferedWriter(sw) sr = socket.SocketIO(self.socket, 'r') self.inp = KQMLReader(io.BufferedReader(sr)) return True except socket.error as e: if verbose: logger.error(e) return False def register(self): if self.name is not None: perf = KQMLPerformative('register') perf.set('name', self.name) if self.group_name is not None: try: if self.group_name.startswith('('): perf.sets('group', self.group_name) else: perf.set('group', self.group_name) except IOError: logger.error('bad group name: ' + self.group_name) self.send(perf) def ready(self): msg = KQMLPerformative('tell') content = KQMLList(['module-status', 'ready']) msg.set('content', content) self.send(msg) def exit(self, n): if self.is_application: sys.exit(n) else: if self.dispatcher is not None: self.dispatcher.shutdown() sys.exit(n) def receive_eof(self): self.exit(0) def receive_message_missing_verb(self, msg): self.error_reply(msg, 'missing verb in performative') def receive_message_missing_content(self, msg): self.error_reply(msg, 'missing content in performative') def receive_ask_if(self, msg, content): self.error_reply(msg, 'unexpected performative: ask-if') def receive_ask_all(self, msg, content): self.error_reply(msg, 'unexpected performative: ask-all') def receive_ask_one(self, msg, content): self.error_reply(msg, 'unexpected performative: ask-one') def receive_stream_all(self, msg, content): self.error_reply(msg, 'unexpected performative: stream-all') def receive_tell(self, msg, content): logger.error('unexpected performative: tell') def receive_untell(self, msg, content): self.error_reply(msg, 'unexpected performative: untell') def receive_deny(self, msg, content): self.error_reply(msg, 'unexpected performative: deny') def receive_insert(self, msg, content): self.error_reply(msg, 'unexpected performative: insert') def receive_uninsert(self, msg, content): self.error_reply(msg, 'unexpected performative: uninsert') def receive_delete_one(self, msg, content): self.error_reply(msg, 'unexpected performative: delete-one') def receive_delete_all(self, msg, content): self.error_reply(msg, 'unexpected performative: delete-all') def receive_undelete(self, msg, content): self.error_reply(msg, 'unexpected performative: undelete') def receive_achieve(self, msg, content): self.error_reply(msg, 'unexpected performative: achieve') def receive_advertise(self, msg, content): self.error_reply(msg, 'unexpected performative: advertise') def receive_unadvertise(self, msg, content): self.error_reply(msg, 'unexpected performative: unadvertise') def receive_subscribe(self, msg, content): self.error_reply(msg, 'unexpected performative: subscribe') def receive_standby(self, msg, content): self.error_reply(msg, 'unexpected performative: standby') def receive_register(self, msg, content): self.error_reply(msg, 'unexpected performative: register') def receive_forward(self, msg, content): self.error_reply(msg, 'unexpected performative: forward') def receive_broadcast(self, msg, content): self.error_reply(msg, 'unexpected performative: broadcast') def receive_transport_address(self, msg, content): self.error_reply(msg, 'unexpected performative: transport-address') def receive_broker_one(self, msg, content): self.error_reply(msg, 'unexpected performative: broker-one') def receive_broker_all(self, msg, content): self.error_reply(msg, 'unexpected performative: broker-all') def receive_recommend_one(self, msg, content): self.error_reply(msg, 'unexpected performative: recommend-one') def receive_recommend_all(self, msg, content): self.error_reply(msg, 'unexpected performative: recommend-all') def receive_recruit_one(self, msg, content): self.error_reply(msg, 'unexpected performative: recruit-one') def receive_recruit_all(self, msg, content): self.error_reply(msg, 'unexpected performative: recruit-all') def receive_reply(self, msg, content): logger.error(msg, 'unexpected performative: reply') def receive_request(self, msg, content): self.error_reply(msg, 'unexpected performative: request') def receive_eos(self, msg): self.error_reply(msg, 'unexpected performative: eos') def receive_error(self, msg): logger.error('Error received: "%s"' % msg) def receive_sorry(self, msg): logger.error('unexpected performative: sorry') def receive_ready(self, msg): logger.error(msg, 'unexpected performative: ready') def receive_next(self, msg): self.error_reply(msg, 'unexpected performative: next') def receive_rest(self, msg): self.error_reply(msg, 'unexpected performative: rest') def receive_discard(self, msg): self.error_reply(msg, 'unexpected performative: discard') def receive_unregister(self, msg): logger.error(msg, 'unexpected performative: unregister') def receive_other_performative(self, msg): self.error_reply(msg, 'unexpected performative: ' + str(msg)) def handle_exception(self, ex): logger.error(str(ex)) def send(self, msg): try: msg.write(self.out) except IOError: logger.error('IOError during message sending') pass self.out.write(b'\n') self.out.flush() logger.debug(msg.__repr__()) def send_with_continuation(self, msg, cont): reply_id_base = 'IO-' if self.name is not None: reply_id_base = self.name + '-' reply_id = reply_id_base + str(self.reply_id_counter) self.reply_id_counter += 1 msg.append(':reply-with') msg.append(reply_id) self.dispatcher.add_reply_continuation('%s' % reply_id, cont) self.send(msg) def reply(self, msg, reply_msg): sender = msg.get('sender') if sender is not None: reply_msg.set('receiver', sender) reply_with = msg.get('reply-with') if reply_with is not None: reply_msg.set('in-reply-to', reply_with) self.send(reply_msg) def error_reply(self, msg, comment): reply_msg = KQMLPerformative('error') reply_msg.sets('comment', comment) self.reply(msg, reply_msg)
class CompanionsKQMLModule(KQMLModule): """KQMLModule override to allow for continuous back and forth communication from the running companions agent (facilitator) and this agent. Attributes: debug (bool): helps set the debug level for the loggers accross modules dispatcher (KQMLDispatcher): Dispatcher to be used (from KQMLModule), calls on appropriate functions based on incoming messages, need to keep track of it for proper shutdown host (str): The host of Companions (localhost or an ip address) listen_socket (socket): Socket object the listener will control, receives incoming messages from Companions listener (Thread): Thread running the socket listening loop, calls the dispatcher as well. listener_port (int): port number you want to host the listener on local_out (BufferedWriter): Connection to the listener socker server output, used to send messages on the listener port for Companions to pick up on. name (str): Name of this agent (module), used in registration so this should be set to a new name for each new agent. Currently all instances of this class will have the same name as they are the same type of agent. num_subs (int): The number of subscriptions that the agent has (only used later in Pythonian) out (BufferedWriter): Connection to the Companions KQML socket server, created from send_socket, used by send port (int): port number that Companions is hosted on ready (bool): Boolean that controls the threads looping, overwrites the ready function from KQMLModule reply_id_counter (int): From KQMLModule, used in send_with_continuation adds reply-with and the appropriate reply id send_socket (socket): Socket that will connect to Companions for sending messages. Need to keep track of it to properly close itself initializes to None and only has a socket after calling connect starttime (datetime): the time at which this agent started, used for updating running status in Companions state (str): the state this agent is in, used for updating running status in Companions """ name = 'CompanionsKQMLModule' # pylint: disable=super-init-not-called # We are rewriting the KQMLModule... def __init__(self, host: str = 'localhost', port: int = 9000, listener_port: int = 8950, debug: bool = False): """Override of KQMLModule init to add turn it into a KQML socket server Args: host (str, optional): the host location to connect to via sockets port (int, optional): the port on the host to connect to listener_port (int, optional): the port this class will host its KQML socket server from (the connection end that dispatches requests as needed) debug (bool, optional): Whether to set the level of the logger to DEBUG or INFO - silencing debug errors and only showing needed information. """ # OUTPUTS assert valid_ip(host), 'Host must be local or a valid ip address' self.host = host assert valid_port(port), \ 'port must be valid port number (1024-65535)' self.port = port self.send_socket = None self.out = None # INPUTS assert valid_port(listener_port), \ 'listener_port must be a valid port number (1024-65535)' self.listener_port = listener_port self.dispatcher = None self.listen_socket = socket() self.listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.listen_socket.bind(('', self.listener_port)) self.listen_socket.listen(10) self.local_out = None self.ready = True self.listener = Thread(target=self.listen, args=[]) # FROM KQMLModule self.reply_id_counter = 1 # UPDATES self.starttime = datetime.now() self.state = 'idle' self.num_subs = 0 # LOGGING / DEBUG self.debug = debug if self.debug: LOGGER.setLevel(DEBUG) else: LOGGER.setLevel(INFO) # REGISTER AND START LISTENING LOGGER.info('Starting listener (KQML socket server)...') self.listener.start() self.register() @classmethod # pylint: disable=too-many-arguments # We have 5 arguments plus the class to be created... # 4 arguments from init (keeping init clean and low on arguments), # 1 extra for controlling the check for companions function... def init_check_companions(cls, host: str = None, port: int = None, listener_port: int = None, debug: bool = None, verify_port: bool = False): """Helper method for constructing an agent, with a special helper function if you are running companions on the same machine as this agent (judged by connecting to localhost), without overwriting the default values in init. When the companion is running on the same system as the python agent we check to see if an expected process is running, and if so we look for the listed port number that the process publishes on startup. The check for companions will prioritize executables before looking for something running from source (CompanionsMicroServer64 before CompanionsServer64, allegro for local development with a qrg directory installed at the root of some drive on the system or in the home directory) and will return the first port found (if multiple companions are running). If nothing is found, the default value is relied on. If nothing is running but an old port number is found, you won't connect either way as a companion isn't running - it just might attempt a non-default port. As such, this can be essentially used in place on regular init. Args: host (str, optional): the host value to pass to init, falls back to init defaults. port (int, optional): the port value to pass to init, falls back to init defaults. listener_port (int, optional): the listener_port value to pass to init, falls back to init defaults. debug (bool, optional): the debug value to pass to init, falls back to init defaults. verify_port (bool, optional): whether or not to verify the port number by checking the pid in the portnum.dat file (created by either running companions locally or in an exe) against the pid found on the running process where the portnum.dat file was found Returns: cls: instantiated cls object """ kwargs = {} # repack arguments for a non-default interrupting call if host: kwargs['host'] = host if port: kwargs['port'] = port if listener_port: kwargs['listener_port'] = listener_port if debug: kwargs['debug'] = debug # If no port was passed in and either no host or localhost (no host # would default to local)... if not port and (True if not host else host in LOCALHOST_DEFS): port = check_for_companions(verify_port) if port: kwargs['port'] = port return cls(**kwargs) @classmethod def parse_command_line_args(cls, argv: list = None): """Uses ArgumentParser to parse the args that this is called with. Additional benefit of searching your system for a running Companion if no port is specified and host is local (defaults to local). If no running companion is found use the default values from init. The additional argument -v for verify_pids will assert that the pid's match between what is found on the system and what is put in the file. Returns: cls: instantiated cls object Args: argv (list, optional): argument list (typically from sys.argv) """ if not argv: argv = system_argument_list _, *args = argv # ignore name of file... parser = ArgumentParser(description='Run Pythonian agent.') parser.add_argument('-u', '--url', type=valid_ip, help='url where companions kqml server is hosted') parser.add_argument('-p', '--port', type=valid_port, help='port companions kqml server is open on') parser.add_argument('-l', '--listener_port', type=valid_port, help='port pythonian kqml server is open on') parser.add_argument('-d', '--debug', action='store_true', help='whether or not to log debug messages') parser.add_argument('-v', '--verify_port', action='store_true', help='whether or not to verify the port number by ' 'checking the pid in the portnum.dat file ' '(created by either running companions ' 'locally or in an exe) against the pid found ' 'on the running process where the portnum.dat' ' file was found') args = parser.parse_args(args) return cls.init_check_companions(host=args.url, port=args.port, listener_port=args.listener_port, debug=args.debug, verify_port=args.verify_port) # OUTPUT FUNCTIONS (OVERRIDES): # pylint: disable=arguments-differ def connect(self): """Rewrite of KQMLModule connect, only handles send_socket and output connections""" try: self.send_socket = socket() self.send_socket.connect((self.host, self.port)) socket_write = SocketIO(self.send_socket, 'w') self.out = BufferedWriter(socket_write) except OSError as error_msg: LOGGER.error('Connection failed: %s', error_msg) # Verify that you can send messages... assert self.out is not None, \ 'Connection formed but output (%s) not set.' % (self.out) def send(self, msg: KQMLPerformative): """Override of send from KQMLModule, opens and closes socket around send for proper signaling to Companions Args: msg (KQMLPerformative): message that you are sending to Companions """ self.connect() self.send_generic(msg, self.out) self.send_socket.shutdown(SHUT_RDWR) self.send_socket.close() self.send_socket = None self.out = None def send_on_local_port(self, msg: KQMLPerformative): """Sends a message on the local_out, i.e. sends a message on the listener_port. This is used for some specific functions that are not meant to be handled as a kqml performative. Args: msg (KQMLPerformative): message to be sent """ self.send_generic(msg, self.local_out) def reply_on_local_port(self, msg: KQMLPerformative, reply_msg: KQMLPerformative): """Replies to a message on the local port (listener port) Args: msg (KQMLPerformative): message to reply to reply_msg (KQMLPerformative): message to reply with """ sender = msg.get('sender') if sender is not None: reply_msg.set('receiver', sender) reply_with = msg.get('reply-with') if reply_with is not None: reply_msg.set('in-reply-to', reply_with) self.send_on_local_port(reply_msg) @staticmethod def send_generic(msg: KQMLPerformative, out: BufferedWriter): """Basic send mechanism copied (more or less) from pykqml. Writes the msg as a string to the output buffer then flushes it. Args: msg (KQMLPerformative): Message to be sent out (BufferedWriter): The output to write to, needed for sending to Companions and sending on our own port. """ LOGGER.debug('Sending: %s', msg) try: msg.write(out) except IOError: LOGGER.error('IOError during message sending') out.write(b'\n') out.flush() # INPUT FUNCTIONS (OVERRIDE AND ADDITION): def listen(self): """Socket server, listens for new KQML messages and dispatches them accordingly. Doesn't necessarily need threading; Could just start dispatcher and after it returns accept next connection. This couldn't handle loads of inputs while being bogged down processing. To avoid this issue we thread the dispatching so the functions that get called are run in a separate Thread. We're using ThreadPoolExecutor because sockets use io, io is blocking and threads allow you to not block. """ with ThreadPoolExecutor(max_workers=5) as executor: while self.ready: connection, _ = self.listen_socket.accept() LOGGER.debug('Received connection: %s', connection) socket_write = SocketIO(connection, 'w') self.local_out = BufferedWriter(socket_write) socket_read = SocketIO(connection, 'r') read_input = KQMLReader(BufferedReader(socket_read)) self.dispatcher = KQMLDispatcher(self, read_input, self.name) LOGGER.debug('Starting dispatcher: %s', self.dispatcher) executor.submit(self.dispatcher.start) self.state = 'dispatching' def receive_eof(self): """Override of KQMLModule, shuts down the dispatcher after receiving the end of file (eof) signal. This happens after every message... """ LOGGER.debug('Closing connection on dispatcher: %s', self.dispatcher) self.dispatcher.shutdown() self.dispatcher = None self.state = 'idle' # OVERRIDES TO KQMLModule: def start(self): pass def connect1(self): pass def exit(self, n: int = 0): """Override of KQMLModule; Closes this agent, shuts down the threaded execution loop (by turning off the ready flag), shuts the dispatcher down (if running), and then joins any running threads... Args: n (int, optional): the value to pass along to sys.exit """ LOGGER.info('Shutting down agent: %s', self.name) self.ready = False # may need to wait for threads to stop... if self.dispatcher is not None: self.dispatcher.shutdown() self.listener.join() # COMPANIONS SPECIFIC OVERRIDES: def register(self): """Override of KQMLModule, registers this agent with Companions""" LOGGER.info('Registering...') registration = ( f'(register :sender {self.name} :receiver facilitator :content ' f'("socket://{self.host}:{self.listener_port}" nil nil ' f'{self.listener_port}))') self.send(performative(registration)) def receive_other_performative(self, msg: KQMLPerformative): """Override of KQMLModule default... ping isn't currently supported by pykqml so we handle other to catch ping and otherwise throw an error. Arguments: msg (KQMLPerformative): other type of performative, if ping we reply with a ping update otherwise error """ if msg.head() == 'ping': LOGGER.info('Receive ping... %s', msg) reply_content = ( f'(update :sender {self.name} :content (:agent {self.name} ' f':uptime {self.uptime()} :status :OK :state {self.state} ' f':machine {gethostname()} :subscriptions {self.num_subs}))') self.reply_on_local_port(msg, performative(reply_content)) else: self.error_reply(msg, f'unexpected performative: {msg}') # Everything else (reply, error_reply, handle_exceptions, # send_with_continuation, subscribe_request, subscribe_tell, # and ALL the remaining receive_* functions) is fine as is # HELPERS: def uptime(self) -> str: """Cyc-style time since start. Using the python-dateutil library to do simple relative delta calculations for the uptime. Returns: str: string of the form '(years months days hours minutes seconds)' where years, months, days, etc are the uptime in number of years, months, days, etc. """ time_list = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] diff = relativedelta(datetime.now(), self.starttime) time_diffs = [getattr(diff, time_period) for time_period in time_list] return f'({" ".join(map(str, time_diffs))})' def response_to_query(self, msg: KQMLPerformative, content: KQMLPerformative, results: Any, response_type: str): """Based on the response type, will create a properly formed reply with the results either input as patterns or bound to the arguments from the results. The reply is a tell which is then sent to Companions. Goes through the arguments and the results together to either bind a argument to the result or simple return the result in the place of that argument. The reply content is filled with these argument/result lists (they are listified before appending) before being added to the tell message and subsequently sent off to Companions. Arguments: msg (KQMLPerformative): the message being passed along to reply content (KQMLPerformative): query, starts with a predicate and the remainder is the arguments results (Any): The results of performing the query response_type (str): the given response type, if it is not given or is given to be pattern, the variable will be set to True, otherwise False """ LOGGER.debug('Responding to query: %s, %s, %s', msg, content, results) response_type = response_type is None or response_type == ':pattern' reply_content = KQMLList(content.head()) results_list = results if isinstance(results, list) else [results] result_index = 0 arg_len = len(content.data[1:]) for i, each in enumerate(content.data[1:]): # if argument is a variable, replace in the pattern or bind if str(each[0]) == '?': # if last argument and there's still more in results if i == arg_len and result_index < len(results_list) - 1: pattern = results_list[result_index:] # get remaining list else: pattern = results_list[result_index] reply_with = pattern if response_type else (each, pattern) reply_content.append(listify(reply_with)) result_index += 1 # if not a variable, replace in the pattern. Ignore for bind elif response_type: reply_content.append(each) # no need to wrap reply_content in parens, KQMLList will do that for us reply_msg = f'(tell :sender {self.name} :content {reply_content})' self.reply(msg, performative(reply_msg))
class Pythonian(KQMLModule): name = 'Pythonian' def __init__(self, listener_port=8950, **kwargs): self.listener_port = listener_port super().__init__(name=self.name, **kwargs) self.starttime = datetime.now() self.listen_socket = socket.socket() self.listen_socket.bind(('', self.listener_port)) self.listen_socket.listen(10) self.start() # self.listen() def register(self): perf_string = f'(register :sender {self.name} :receiver facilitator)' perf = performative(perf_string) socket_url = f'"socket://{self.host}:{self.listener_port}"' perf.set('content', KQMLList([socket_url, 'nil', 'nil', self.listener_port])) self.send(perf) def receive_tell(self, msg, content): print(msg) print(content) def receive_eof(self): connection, _ = self.listen_socket.accept() print(connection) # socket_write = socket.SocketIO(connection, 'w') # self.out = io.BufferedWriter(socket_write) socket_read = socket.SocketIO(connection, 'r') inp_reader = KQMLReader(io.BufferedReader(socket_read)) # self.inp = inp_reader self.dispatcher = KQMLDispatcher(self, inp_reader, self.name) self.dispatcher.start() # def listen(self): # while True: # connection, _ = self.listen_socket.accept() # print(connection) # socket_write = socket.SocketIO(connection, 'w') # self.out = io.BufferedWriter(socket_write) # socket_read = socket.SocketIO(connection, 'r') # inp_reader = KQMLReader(io.BufferedReader(socket_read)) # # self.inp = inp_reader # self.dispatcher = KQMLDispatcher(self, inp_reader, self.name) # self.dispatcher.start() def receive_other_performative(self, msg): if msg.head() == 'ping': update_string = ( f'(update :sender {self.name} :content (:agent {self.name} ' f':uptime {self._uptime()} :status :OK :state idle ' f':machine {socket.gethostname()}))') self.reply(msg, performative(update_string)) else: self.error_reply(msg, 'unexpected performative: ' + str(msg)) def _uptime(self): time_list = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] diff = relativedelta(datetime.now(), self.starttime) time_diffs = [getattr(diff, time) for time in time_list] return f'({" ".join(map(str, time_diffs))})'
class KQMLModule(object): def __init__(self, argv=None, **kwargs): defaults = dict(host='localhost', port=6200, is_application=False, testing=False, socket=None, name=None, group_name=None, scan_for_port=False, debug=False, do_register=True) self.dispatcher = None self.MAX_PORT_TRIES = 100 self.reply_id_counter = 1 if isinstance(argv, list): kwargs.update(translate_argv(argv)) elif argv is not None: raise KQMLException("Unusable type for keyord argument `argv`.") for kw, arg in kwargs.items(): if kw not in defaults.keys(): raise ValueError('Unexpected keyword argument: %s' % kw) else: defaults.pop(kw) self.__setattr__(kw, arg) for kw, arg in defaults.items(): self.__setattr__(kw, arg) if self.debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.INFO) if not self.testing: self.out = None self.inp = None logger.info('Using socket connection') conn = self.connect(self.host, self.port) if not conn: logger.error('Connection failed') self.exit(-1) assert self.inp is not None and self.out is not None, \ "Connection formed but input (%s) and output (%s) not set." % \ (self.inp, self.out) else: logger.info('Using stdio connection') self.out = io.BytesIO() self.inp = KQMLReader(io.BytesIO()) self.dispatcher = KQMLDispatcher(self, self.inp, self.name) if self.do_register: self.register() def start(self): if not self.testing: self.dispatcher.start() def subscribe_request(self, req_type): msg = KQMLPerformative('subscribe') content = KQMLList('request') content.append('&key') content.set('content', KQMLList.from_string('(%s . *)' % req_type)) msg.set('content', content) self.send(msg) def subscribe_tell(self, tell_type): msg = KQMLPerformative('subscribe') content = KQMLList('tell') content.append('&key') content.set('content', KQMLList.from_string('(%s . *)' % tell_type)) msg.set('content', content) self.send(msg) def connect(self, host=None, startport=None): if host is None: host = self.host if startport is None: startport = self.port if not self.scan_for_port: return self.connect1(host, startport, True) else: maxtries = self.MAX_PORT_TRIES for port in range(startport, startport + maxtries): conn = self.connect1(host, port, False) if conn: return True logger.error('Failed to connect to ' + host + ':' + \ startport + '-' + port) return False def connect1(self, host, port, verbose=True): try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((host, port)) sw = socket.SocketIO(self.socket, 'w') self.out = io.BufferedWriter(sw) sr = socket.SocketIO(self.socket, 'r') self.inp = KQMLReader(io.BufferedReader(sr)) return True except socket.error as e: if verbose: logger.error(e) return False def register(self): if self.name is not None: perf = KQMLPerformative('register') perf.set('name', self.name) if self.group_name is not None: try: if self.group_name.startswith('('): perf.sets('group', self.group_name) else: perf.set('group', self.group_name) except IOError: logger.error('bad group name: ' + self.group_name) self.send(perf) def ready(self): msg = KQMLPerformative('tell') content = KQMLList(['module-status', 'ready']) msg.set('content', content) self.send(msg) def exit(self, n): if self.is_application: sys.exit(n) else: if self.dispatcher is not None: self.dispatcher.shutdown() sys.exit(n) def stop_waiting(self): raise StopWaitingSignal() def receive_eof(self): self.exit(0) def receive_message_missing_verb(self, msg): self.error_reply(msg, 'missing verb in performative') def receive_message_missing_content(self, msg): self.error_reply(msg, 'missing content in performative') def receive_ask_if(self, msg, content): self.error_reply(msg, 'unexpected performative: ask-if') def receive_ask_all(self, msg, content): self.error_reply(msg, 'unexpected performative: ask-all') def receive_ask_one(self, msg, content): self.error_reply(msg, 'unexpected performative: ask-one') def receive_stream_all(self, msg, content): self.error_reply(msg, 'unexpected performative: stream-all') def receive_tell(self, msg, content): logger.error('unexpected performative: tell') def receive_untell(self, msg, content): self.error_reply(msg, 'unexpected performative: untell') def receive_deny(self, msg, content): self.error_reply(msg, 'unexpected performative: deny') def receive_insert(self, msg, content): self.error_reply(msg, 'unexpected performative: insert') def receive_uninsert(self, msg, content): self.error_reply(msg, 'unexpected performative: uninsert') def receive_delete_one(self, msg, content): self.error_reply(msg, 'unexpected performative: delete-one') def receive_delete_all(self, msg, content): self.error_reply(msg, 'unexpected performative: delete-all') def receive_undelete(self, msg, content): self.error_reply(msg, 'unexpected performative: undelete') def receive_achieve(self, msg, content): self.error_reply(msg, 'unexpected performative: achieve') def receive_advertise(self, msg, content): self.error_reply(msg, 'unexpected performative: advertise') def receive_unadvertise(self, msg, content): self.error_reply(msg, 'unexpected performative: unadvertise') def receive_subscribe(self, msg, content): self.error_reply(msg, 'unexpected performative: subscribe') def receive_standby(self, msg, content): self.error_reply(msg, 'unexpected performative: standby') def receive_register(self, msg, content): self.error_reply(msg, 'unexpected performative: register') def receive_forward(self, msg, content): self.error_reply(msg, 'unexpected performative: forward') def receive_broadcast(self, msg, content): self.error_reply(msg, 'unexpected performative: broadcast') def receive_transport_address(self, msg, content): self.error_reply(msg, 'unexpected performative: transport-address') def receive_broker_one(self, msg, content): self.error_reply(msg, 'unexpected performative: broker-one') def receive_broker_all(self, msg, content): self.error_reply(msg, 'unexpected performative: broker-all') def receive_recommend_one(self, msg, content): self.error_reply(msg, 'unexpected performative: recommend-one') def receive_recommend_all(self, msg, content): self.error_reply(msg, 'unexpected performative: recommend-all') def receive_recruit_one(self, msg, content): self.error_reply(msg, 'unexpected performative: recruit-one') def receive_recruit_all(self, msg, content): self.error_reply(msg, 'unexpected performative: recruit-all') def receive_reply(self, msg, content): logger.error(msg, 'unexpected performative: reply') def receive_request(self, msg, content): self.error_reply(msg, 'unexpected performative: request') def receive_eos(self, msg): self.error_reply(msg, 'unexpected performative: eos') def receive_error(self, msg): logger.error('Error received: "%s"' % msg) def receive_sorry(self, msg): logger.error('unexpected performative: sorry') def receive_ready(self, msg): logger.error(msg, 'unexpected performative: ready') def receive_next(self, msg): self.error_reply(msg, 'unexpected performative: next') def receive_rest(self, msg): self.error_reply(msg, 'unexpected performative: rest') def receive_discard(self, msg): self.error_reply(msg, 'unexpected performative: discard') def receive_unregister(self, msg): logger.error(msg, 'unexpected performative: unregister') def receive_other_performative(self, msg): self.error_reply(msg, 'unexpected performative: ' + str(msg)) def handle_exception(self, ex): logger.error(str(ex)) def send(self, msg): try: msg.write(self.out) except IOError: logger.error('IOError during message sending') pass self.out.write(b'\n') self.out.flush() logger.debug(msg.__repr__()) def send_with_continuation(self, msg, cont): reply_id_base = 'IO-' if self.name is not None: reply_id_base = self.name + '-' reply_id = reply_id_base + str(self.reply_id_counter) self.reply_id_counter += 1 msg.append(':reply-with') msg.append(reply_id) self.dispatcher.add_reply_continuation('%s' % reply_id, cont) self.send(msg) def reply(self, msg, reply_msg): sender = msg.get('sender') if sender is not None: reply_msg.set('receiver', sender) reply_with = msg.get('reply-with') if reply_with is not None: reply_msg.set('in-reply-to', reply_with) self.send(reply_msg) def error_reply(self, msg, comment): reply_msg = KQMLPerformative('error') reply_msg.sets('comment', comment) self.reply(msg, reply_msg)
class Pythonian(KQMLModule): """Wrapper around the KQMLModule to create a simple Companions integration. KQML gives us default communications protocols that we have altered to fit both Lisp standards and Companion expectations. In addition, we have added some functions that interface with Companions both by exposing the Companions style communication functions and by adding helpful generic wrappers around those functions to make integrating even more simple. Extends: KQMLModule Variables: name {str} -- The name of the agent to register with """ name = "Pythonian" def __init__(self, **kwargs): # Call the parent class' constructor which sends a registration # message, setting the agent's name to be recognized by the # Facilitator. if 'localPort' in kwargs: # keyword is camelCase but, self.local_port = kwargs['localPort'] # variable is snake_case del kwargs['localPort'] else: self.local_port = 8950 if 'port' in kwargs: port = kwargs['port'] del kwargs['port'] else: port = 9000 super(Pythonian, self).__init__(name=self.name, port=port, **kwargs) self.starttime = datetime.now() # tracking functions related to asks and achieves self.achieves = {} self.asks = {} # subscription stuff self.subscribers = {} # query:[subscribe,msgs] self.subcribe_data_old = {} self.subcribe_data_new = {} self.polling_interval = 1 self.poller = threading.Thread( target=self._poll_for_subscription_updates, args=[]) # Finally, start the listener for incoming messages self.listen_socket = socket.socket() self.listen_socket.bind(('', self.local_port)) self.listen_socket.listen(10) self.listener = threading.Thread(target=self._listen, args=[]) # ready to go self.state = 'idle' self.ready = True self.poller.start() self.listener.start() ########################################################################### # Utility functions # ########################################################################### def add_achieve(self, name, func): """Adds the given function (func) to the dictionary of achieves under the key of the given name. Arguments: name {str} -- key in achieves dictionary func {function} -- value paired to key, function to be called on achieve (with given name) """ self.achieves[name] = func def achieve_on_agent(self, receiver, data): """Sends a KQML message to the proper receiver with the data formatted properly as a list. Arguments: receiver {str} -- name of the receiver data {[type]} -- anything """ msg = KQMLPerformative('achieve') msg.set('sender', self.name) msg.set('receiver', receiver) msg.set('content', listify(data)) # pylint: disable=no-member # pylint was unable to pick up on the host and port variables being in # a defaults dict which is then used to do __setattr__ with the key # value pairs from the dict. self.connect(self.host, self.port) self.send(msg) # TODO - Does this need to close after? # self._close_socket() def add_ask(self, name, func, pattern, subscribable=False): """Adds the given function (func) to the dictionary of asks under the key of the given name. If subscribable is true then we also add the pattern to our subscription dictionary and advertise it. Arguments: name {str} -- key in asks dictionary func {function} -- value paired to key, function to be called on ask pattern {str} -- name of subscribers Keyword Arguments: subscribable {bool} -- [description] (default: {False}) """ self.asks[name] = func if subscribable: self.subscribers[pattern] = [] self.advertise_subscribe(pattern) def advertise(self, pattern): """Connects to the host, then builds and sends a message to the facilitator with the pattern input as the content, then closes the connection. Arguments: pattern {[type]} -- content to be sent """ # pylint: disable=no-member # pylint was unable to pick up on the host and port variables being in # a defaults dict which is then used to do __setattr__ with the key # value pairs from the dict. self.connect(self.host, self.port) reply_id = f'id{self.reply_id_counter}' self.reply_id_counter += 1 content = KQMLPerformative('ask-all') content.set('receiver', self.name) content.set('in-reply-to', reply_id) content.set('content', pattern) msg = KQMLPerformative('advertise') msg.set('sender', self.name) msg.set('receiver', 'facilitator') msg.set('reply-with', reply_id) msg.set('content', content) self.send(msg) self._close_socket() def advertise_subscribe(self, pattern): """Connects to the host, then builds and sends a message to the facilitator with the pattern input as the content, then closes the connection. Arguments: pattern {[type]} -- content to be sent """ # pylint: disable=no-member # pylint was unable to pick up on the host and port variables being in # a defaults dict which is then used to do __setattr__ with the key # value pairs from the dict. self.connect(self.host, self.port) reply_id = f'id{self.reply_id_counter}' self.reply_id_counter += 1 content = KQMLPerformative('ask-all') content.set('receiver', self.name) content.set('in-reply-to', reply_id) content.set('content', pattern) subscribe = KQMLPerformative('subscribe') subscribe.set('receiver', self.name) subscribe.set('in-reply-to', reply_id) subscribe.set('content', content) msg = KQMLPerformative('advertise') msg.set('sender', self.name) msg.set('receiver', 'facilitator') msg.set('reply-with', reply_id) msg.set('content', subscribe) self.send(msg) self._close_socket() def update_query(self, query, *args): """Looks to see if the arguments to query have changes since last time, if so it will update those arguments in the subscribe_data_new dict. Arguments: query {[type]} -- string representing the query *args {[type]} -- anything you would input into the query """ if query in self.subcribe_data_old: if self.subcribe_data_old[query] != args: LOGGER.debug("Updating %s with %s", query, args) self.subcribe_data_new[query] = args def insert_data(self, receiver, data, wm_only=False): """Takes the data input by the user and processes it into an insert message which is subsequently sent off to Companions. Arguments: receiver {str} -- name of the receiver data {[type]} -- anything that can be listified Keyword Arguments: wm_only {bool} -- whether or not this should only be inserted into the working memory (default: {False}) """ msg = KQMLPerformative('insert') msg.set('sender', self.name) msg.set('receiver', receiver) if wm_only: msg.append(':wm-only?') msg.set('content', listify(data)) # pylint: disable=no-member # pylint was unable to pick up on the host and port variables being in # a defaults dict which is then used to do __setattr__ with the key # value pairs from the dict. self.connect(self.host, self.port) self.send(msg) # TODO - Does this need to close after? # self._close_socket() def insert_to_microtheory(self, receiver, data, mt_name, wm_only=False): """Inserts a fact into the given microtheory using ist-Information Arguments: receiver {str} -- passed through to insert data data {[type]} -- anything that can be listified, the fact to insert mt_name {str} -- microtheory name Keyword Arguments: wm_only {bool} -- whether or not this fact should go to working memory only or just the KB (default: {False}) """ new_data = '(ist-Information {} {})'.format(mt_name, data) self.insert_data(receiver, new_data, wm_only) def insert_microtheory(self, receiver, data_list, mt_name, wm_only=False): """Inserts a list of facts into the given microtheory Arguments: receiver {str} -- passed through to insert data data_list {list} -- list of facts to be added, facts can be anything you can listify. mt_name {str} -- microtheory name Keyword Arguments: wm_only {bool} -- whether or not this fact should go to working memory only or just the KB (default: {False}) """ for data in data_list: self.insert_to_microtheory(receiver, data, mt_name, wm_only) ########################################################################### # Overrides # ########################################################################### def register(self): """Overrides the KQMLModule default and uses Companions standards to send proper registration. """ if self.name is not None: perf = KQMLPerformative('register') perf.set('sender', self.name) perf.set('receiver', 'facilitator') # pylint: disable=no-member # pylint was unable to pick up on the host variable being in a # defaults dict which is then used to do __setattr__ with the key # value pairs from the dict. socket_url = f'"socket://{self.host}:{self.local_port}"' content = KQMLList([socket_url, 'nil', 'nil', self.local_port]) perf.set('content', content) self.send(perf) def receive_eof(self): """Override of the KQMLModule default which exits on eof, we instead just pass and do nothing on end of file. """ # pylint: disable=unnecessary-pass # When there are no comments in this function (document string # included) this method passes the unnecessary-pass but fails on the # missing-docstring. This is a pylint bug. pass def receive_ask_one(self, msg, content): """Override of default ask one, creates Companions style responses. Gets the arguments bindings from the cdr of the content. The predicate (car) is then used to find the function bound to the ask predicate, and that function is called with the bounded argument list unpacked into it's own inputs. The resulting query is then passed along to the _response_to_query helper which will properly respond to patterns or bindings based on out response type. Arguments: msg {KQMLPerformative} -- pred and response type content {KQMLPerformative} -- arguments of the ask, passed to pred """ bounded = [] for each in content.data[1:]: if str(each[0]) != '?': bounded.append(each) results = self.asks[content.head()](*bounded) self._response_to_query(msg, content, results, msg.get('response')) def receive_achieve(self, msg, content): """Overrides the default KQMLModule receive for achieves and instead does basic error checking before attempting the action by calling the proper ask function with the arguments passed along as inputs. Arguments: msg {KQMLPerformative} -- predicate/ signifier of task content {KQMLPerformative} -- action task is referring to """ if content.head() == 'task': action = content.get('action') if action: if action.head() in self.achieves: try: args = action.data[1:] results = self.achieves[action.head()](*args) LOGGER.debug("Return of achieve: %s", results) reply = KQMLPerformative('tell') reply.set('sender', self.name) reply.set('content', listify(results)) self.reply(msg, reply) # pylint: disable=broad-except # The above try can throw KQMLBadPerformativeException, # KQMLException, ValueError, and StopWaitingSignal at the # least. To stay simple and more in-line with PEP8 we are # logging the traceback and the user should be made aware # of the fact that an error occurred via the error_reply. # For these reasons the 'broad-except' is valid here. except Exception: LOGGER.debug(traceback.print_exc()) error_msg = 'An error occurred while executing: ' self.error_reply(msg, error_msg + action.head()) else: self.error_reply(msg, 'Unknown action: ' + action.head()) else: self.error_reply(msg, 'No action for achieve task provided') else: error_msg = 'Unexpected achieve command: ' self.error_reply(msg, error_msg + content.head()) def receive_tell(self, msg, content): """Override default KQMLModule tell to simply log the content and reply with nothing Arguments: msg {KQMLPerformative} -- tell to be passed along in reply content {KQMLPerformative} -- tell from companions to be logged """ LOGGER.debug('received tell: %s', content) # lazy logging reply_msg = KQMLPerformative('tell') reply_msg.set('sender', self.name) reply_msg.set('content', None) self.reply(msg, reply_msg) def receive_subscribe(self, msg, content): """Override of KQMLModule default, expects a performative of ask-all. Gets the ask-all query from the message contents (the contents of the content variable is the query that we care about), then checks to see if the query head is in the dictionary of available asks and checks if the query string is in the dictionary of subscribers. If both of these are true we then append the message to the subscriber query, clean out any previous subscription data, and reply with a tell ok message. Arguments: msg {KQMLPerformative} -- performative to be passed along in reply and stored in the subscribers dictionary content {KQMLPerformative} -- ask-all for a query """ LOGGER.debug('received subscribe: %s', content) # lazy logging if content.head() == 'ask-all': # TODO - track msg ideas and use for filtering query = content.get('content') query_string = query.to_string() if query.head() in self.asks and query_string in self.subscribers: self.subscribers[query_string].append(msg) self.subcribe_data_old[query_string] = None self.subcribe_data_new[query_string] = None reply_msg = KQMLPerformative('tell') reply_msg.set(':sender', self.name) reply_msg.set('content', ':ok') self.reply(msg, reply_msg) def receive_other_performative(self, msg): """Override of KQMLModule default... ping isn't currently supported by pykqml so we handle other to catch ping and otherwise throw an error. Arguments: msg {KQMLPerformative} -- other type of performative, if ping we reply with a ping update otherwise error """ if msg.head() == 'ping': reply_content = KQMLList([':agent', self.name]) reply_content.append(':uptime') reply_content.append(self._uptime()) # TODO - check if .set('status', ':OK') can be used here instead reply_content.append(':status') reply_content.append(':OK') reply_content.append(':state') reply_content.append('idle') reply_content.append(':machine') reply_content.append(socket.gethostname()) reply = KQMLPerformative('update') reply.set('sender', self.name) # reply.set('receiver', msg.get('sender')) # reply.set('in-reply-to', msg.get('reply-with')) reply.set('content', reply_content) self.reply(msg, reply) else: self.error_reply(msg, 'unexpected performative: ' + str(msg)) ########################################################################### # Threading # ########################################################################### def _poll_for_subscription_updates(self): """Goes through the subscription updates as they come in and properly respond to the query (with _response_to_query). """ LOGGER.debug("Running subcription poller") while self.ready: for query, new_data in self.subcribe_data_new.items(): if new_data is not None: for msg in self.subscribers[query]: ask = msg.get('content') query = ask.get('content') LOGGER.debug("Sending subscribe update for %s", query) res_type = ask.get('response') self._response_to_query(msg, query, new_data, res_type) self.subcribe_data_old[query] = new_data for query, _ in self.subcribe_data_new.items(): self.subcribe_data_new[query] = None time.sleep(self.polling_interval) def _listen(self): """Sets up input and output socket connections to our listener. Infinite loop while ready to connect to our listener socket. On connect we get the write socket as a Buffered Writer and the read socket as a KQML Reader (which passes through a Buffered Reader). The reader is then attached to a KQML Dispatcher which is subsequently started, and passed along to the executor (which is a thread pool manager). """ LOGGER.debug('listening') with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: while self.ready: connection, _ = self.listen_socket.accept() LOGGER.debug('received connection') socket_write = socket.SocketIO(connection, 'w') self.out = io.BufferedWriter(socket_write) socket_read = socket.SocketIO(connection, 'r') read_input = KQMLReader(io.BufferedReader(socket_read)) self.dispatcher = KQMLDispatcher(self, read_input, self.name) executor.submit(self.dispatcher.start) ########################################################################### # General helpers # ########################################################################### def _response_to_query(self, msg, content, results, response_type): """Based on the response type, will create a properly formed reply with the results either input as patterns or bound to the arguments from the results. The reply is a tell which is then sent to Companions. Goes through the arguments and the results together to either bind a argument to the result or simple return the result in the place of that argument. The reply content is filled with these argument/result lists (they are listified before appending) before being added to the tell message and subsequently sent off to Companions. Arguments: msg {KQMLPerformative} -- the message being passed along to reply content {[type]} -- query, starts with a predicate and the remainder is the arguments results {[type]} -- The results of performing the query response_type {[type]} -- the given response type, if it is not given or is given to be pattern, the variable will be set to True, otherwise False. """ response_type = response_type is None or response_type == ':pattern' reply_content = KQMLList(content.head()) results_list = results if isinstance(results, list) else [results] result_index = 0 arg_len = len(content.data[1:]) for i, each in enumerate(content.data[1:]): # if argument is a variable if str(each[0]) == '?': # if last argument and there's still more in results if i == arg_len and result_index < len(results_list)-1: # get the remaining list pattern = results_list[result_index:] # pattern or binding... reply_with = pattern if response_type else (each, pattern) reply_content.append(listify(reply_with)) else: # same logic as above, just doesn't get the remaining list pattern = results_list[result_index] reply_with = pattern if response_type else (each, pattern) reply_content.append(listify(reply_with)) result_index += 1 else: if response_type: # only add the arguments if this is pattern reply_content.append(each) reply_msg = KQMLPerformative('tell') reply_msg.set('sender', self.name) reply_msg.set('content', reply_content) self.reply(msg, reply_msg) def _close_socket(self): """shutsdown the dispatcher and closes the socket""" self.dispatcher.shutdown() self.socket.close() def _uptime(self): """Cyc-style time since start (includes leap years) Returns: str -- string of the form '(years months days hours minutes seconds)' where years, months, days, etc are the uptime in number of years, months, days, etc. """ now = datetime.now() years = now.year - self.starttime.year # months if now.year == self.starttime.year: months = now.month - self.starttime.month else: months = 12 - self.starttime.month + now.month # days if now.month == self.starttime.month: days = now.day - self.starttime.day elif self.starttime.month in [1, 3, 5, 7, 8, 10, 12]: days = 31 - self.starttime.day + now.day elif self.starttime.month in [4, 6, 9, 11]: days = 30 - self.starttime.day + now.day else: days = 28 - self.starttime.day + now.day year_list = range(self.starttime.year, now.year+1) extra_days = sum(map(int, [is_leap(year) for year in year_list])) days += extra_days # hours if self.starttime.day == now.day: hours = now.hour - self.starttime.hour else: hours = 24 - self.starttime.hour + now.hour # minutes if self.starttime.hour == now.hour: minutes = now.minute - self.starttime.minute else: minutes = 60 - self.starttime.minute + now.minute # seconds if self.starttime.minute == now.minute: seconds = now.second - self.starttime.second else: seconds = 60 - self.starttime.second + now.second time_list = [years, months, days, hours, minutes, seconds] # return '({})'.format(" ".join(map(str, time_list))) return f'({" ".join(map(str, time_list))})'